Skip to content
Merged
Show file tree
Hide file tree
Changes from 75 commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
5a34d25
feat(be): store a revocable session on the account reference
sea-snake Aug 22, 2026
87abadb
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Aug 25, 2026
dd33ee8
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Aug 25, 2026
ad4a64f
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Aug 25, 2026
682561b
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Aug 31, 2026
362beda
feat: a session takes the idle bound its ceremony asked for
sea-snake Aug 31, 2026
f0f2360
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Aug 31, 2026
36c5665
merge: carry the unit rename into the creation path
sea-snake Aug 31, 2026
75ab548
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Aug 31, 2026
2c20742
feat: every session gets an idle bound, seven days unless asked other…
sea-snake Aug 31, 2026
fee6499
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 1, 2026
fbb7bd2
merge: a dropped row refuses before it removes anything
sea-snake Sep 1, 2026
8398b20
fix(be): unindex a row's sessions before the counters retire its appl…
sea-snake Sep 1, 2026
03dec28
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 1, 2026
4b63e2f
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 4, 2026
5c4c636
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 4, 2026
a5bea93
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 4, 2026
71c93b1
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 4, 2026
d06d47b
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 4, 2026
ae3dc61
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 4, 2026
dd20267
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 4, 2026
2ab7747
refactor(be): name a session by key rather than by its parts
sea-snake Sep 5, 2026
581a082
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
5432935
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
cbab251
feat: name a session by an id nothing else can produce
sea-snake Sep 5, 2026
a3b78e1
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
7583f8d
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
dba5a15
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
79c52bc
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
3196c7c
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
5ba8300
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
2525073
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
668e7e9
fix: a new session resolves without waiting for the backfill
sea-snake Sep 5, 2026
7655c99
fix: a session count that cannot be moved rolls the message back
sea-snake Sep 5, 2026
3e9c4e9
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
a6fb9a6
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
926da38
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
fb82fdf
refactor: say why the session count cannot fail instead of trapping o…
sea-snake Sep 5, 2026
c52c336
refactor: derive the session index and count where the row is written
sea-snake Sep 5, 2026
73310ab
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
f5e6d49
refactor(be): name the account reference list rather than a row
sea-snake Sep 5, 2026
d8d2b0d
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 5, 2026
622ee33
refactor(be): name the account reference list rather than a row
sea-snake Sep 5, 2026
65fcf55
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 6, 2026
633f3de
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 6, 2026
6376bd6
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 6, 2026
8915093
test(be): pin that an account write needs an identity that exists
sea-snake Sep 6, 2026
6290b4e
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 6, 2026
c125ffc
refactor(be): a browser, not a session device
sea-snake Sep 6, 2026
c6346b6
refactor(be): a dropped browser's sessions go in the write that dropp…
sea-snake Sep 6, 2026
2616def
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 6, 2026
ec0b771
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 6, 2026
ff06ce6
refactor(be): the write path takes the identity record, and stores it
sea-snake Sep 6, 2026
ad956be
fix(be): refuse a session that is already over
sea-snake Sep 6, 2026
ebed771
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 7, 2026
1bb1969
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 7, 2026
40796a7
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 7, 2026
b27036b
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 7, 2026
2c4de6e
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 7, 2026
9c16933
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 7, 2026
9a07b8c
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 7, 2026
15f4d36
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 7, 2026
21e4313
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 7, 2026
46f414b
perf(internet_identity): bound the session index too
sea-snake Sep 7, 2026
f3f2ba0
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 7, 2026
b1d4469
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 7, 2026
c652d54
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 7, 2026
483271c
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 8, 2026
f71af57
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 8, 2026
951ae05
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 8, 2026
d5218b4
fix(internet_identity): report the session index and id allocator's m…
sea-snake Sep 8, 2026
5d4215f
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 8, 2026
c132027
chore: merge feat/session-device-key-proof
sea-snake Sep 8, 2026
fc15af3
chore: merge feat/session-device-key-proof
sea-snake Sep 8, 2026
8ca86a0
feat(internet_identity): count each browser's sessions where they are…
sea-snake Sep 8, 2026
c06643d
chore: merge feat/session-device-key-proof
sea-snake Sep 8, 2026
32cb0fe
chore: merge feat/session-device-key-proof, and take the description …
sea-snake Sep 8, 2026
e4e03b7
docs(internet_identity): state why one write, without narrating the loop
sea-snake Sep 8, 2026
fd3c05a
chore: merge feat/session-device-key-proof
sea-snake Sep 9, 2026
9de8210
fix(be): a sign-in writes its own origin, and the gate sweeps the rest
sea-snake Sep 9, 2026
d2276f9
chore: merge feat/session-device-key-proof
sea-snake Sep 9, 2026
226012e
chore: merge feat/session-device-key-proof
sea-snake Sep 9, 2026
185a381
Merge branch 'feat/session-device-key-proof' into feat/session-create
sea-snake Sep 9, 2026
dbe4a13
refactor(session): a locator, verified keys, and the illegal state sa…
sea-snake Sep 9, 2026
f626fdd
chore(storage): drop the account-principal helper that grew a twin
sea-snake Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/frontend/src/lib/generated/internet_identity_idl.js
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ export const idlFactory = ({ IDL }) => {
});
const BrowserInfo = IDL.Record({
'id' : IDL.Nat32,
'session_count' : IDL.Nat32,
'name' : IDL.Text,
'created_at' : Timestamp,
'last_used' : Timestamp,
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/src/lib/generated/internet_identity_types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1541,6 +1541,10 @@ export type SessionDelegationError = { 'NoSuchDelegation' : null } |
*/
export interface BrowserInfo {
'id' : number,
/**
* Sessions this browser holds. Zero means it is signed in to nothing.
*/
'session_count' : number,
'name' : string,
'created_at' : Timestamp,
/**
Expand Down
2 changes: 2 additions & 0 deletions src/internet_identity/internet_identity.did
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,8 @@ type BrowserInfo = record {
created_at : Timestamp;
// Advanced by a sign-in from this browser and by every session refresh it drives.
last_used : Timestamp;
// Sessions this browser holds. Zero means it is signed in to nothing.
session_count : nat32;
};

type IdentityInfo = record {
Expand Down
78 changes: 65 additions & 13 deletions src/internet_identity/src/account_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,11 @@ fn should_create_account_for_origin() {
use crate::state::{storage_borrow_mut, storage_replace};

storage_replace(storage_with_salt());
let anchor = storage_borrow_mut(|storage| storage.allocate_anchor(0).unwrap());
let anchor = storage_borrow_mut(|storage| {
let anchor = storage.allocate_anchor(0).unwrap();
storage.write(anchor.clone()).unwrap();
anchor
});
let origin = "https://example.com".to_string();
let name = "Alice".to_string();

Expand All @@ -423,7 +427,11 @@ fn should_fail_to_create_accounts_above_max() {
use crate::storage::MAX_ANCHOR_ACCOUNTS;

storage_replace(storage_with_salt());
let anchor = storage_borrow_mut(|storage| storage.allocate_anchor(0).unwrap());
let anchor = storage_borrow_mut(|storage| {
let anchor = storage.allocate_anchor(0).unwrap();
storage.write(anchor.clone()).unwrap();
anchor
});
let name = "Alice".to_string();
for i in 0..=MAX_ANCHOR_ACCOUNTS {
let origin = format!("https://example-{i}.com");
Expand All @@ -443,7 +451,11 @@ fn should_fail_to_update_default_accounts_above_max() {
use crate::storage::MAX_ANCHOR_ACCOUNTS;

storage_replace(storage_with_salt());
let anchor = storage_borrow_mut(|storage| storage.allocate_anchor(0).unwrap());
let anchor = storage_borrow_mut(|storage| {
let anchor = storage.allocate_anchor(0).unwrap();
storage.write(anchor.clone()).unwrap();
anchor
});
let name = "Alice".to_string();
for i in 0..MAX_ANCHOR_ACCOUNTS {
let origin = format!("https://example-{i}.com");
Expand All @@ -468,7 +480,11 @@ fn should_get_accounts_for_origin() {
use crate::state::{storage_borrow_mut, storage_replace};

storage_replace(storage_with_salt());
let anchor = storage_borrow_mut(|storage| storage.allocate_anchor(0).unwrap());
let anchor = storage_borrow_mut(|storage| {
let anchor = storage.allocate_anchor(0).unwrap();
storage.write(anchor.clone()).unwrap();
anchor
});
let origin = "https://example.com".to_string();
let name = "Alice".to_string();
let name_two = "Bob".to_string();
Expand Down Expand Up @@ -506,8 +522,16 @@ fn should_only_get_own_accounts_for_origin() {
use crate::state::{storage_borrow_mut, storage_replace};

storage_replace(storage_with_salt());
let anchor = storage_borrow_mut(|storage| storage.allocate_anchor(0).unwrap());
let anchor_two = storage_borrow_mut(|storage| storage.allocate_anchor(0).unwrap());
let anchor = storage_borrow_mut(|storage| {
let anchor = storage.allocate_anchor(0).unwrap();
storage.write(anchor.clone()).unwrap();
anchor
});
let anchor_two = storage_borrow_mut(|storage| {
let anchor = storage.allocate_anchor(0).unwrap();
storage.write(anchor.clone()).unwrap();
anchor
});
let origin = "https://example.com".to_string();
let name = "Alice".to_string();
let name_two = "Bob".to_string();
Expand Down Expand Up @@ -553,7 +577,11 @@ fn should_update_account_for_origin() {
use crate::state::{storage_borrow_mut, storage_replace};

storage_replace(storage_with_salt());
let anchor = storage_borrow_mut(|storage| storage.allocate_anchor(0).unwrap());
let anchor = storage_borrow_mut(|storage| {
let anchor = storage.allocate_anchor(0).unwrap();
storage.write(anchor.clone()).unwrap();
anchor
});
let origin = "https://example.com".to_string();
let name = "Alice".to_string();
let name_two = "Bob".to_string();
Expand Down Expand Up @@ -633,7 +661,11 @@ fn should_update_default_account_for_origin() {
use crate::state::{storage_borrow_mut, storage_replace};

storage_replace(storage_with_salt());
let anchor = storage_borrow_mut(|storage| storage.allocate_anchor(0).unwrap());
let anchor = storage_borrow_mut(|storage| {
let anchor = storage.allocate_anchor(0).unwrap();
storage.write(anchor.clone()).unwrap();
anchor
});
let origin = "https://example.com".to_string();
let name = "Alice".to_string();
let name_two = "Bob".to_string();
Expand Down Expand Up @@ -787,7 +819,11 @@ fn should_get_default_account_for_origin() {
use crate::state::{storage_borrow_mut, storage_replace};

storage_replace(storage_with_salt());
let anchor = storage_borrow_mut(|storage| storage.allocate_anchor(0).unwrap());
let anchor = storage_borrow_mut(|storage| {
let anchor = storage.allocate_anchor(0).unwrap();
storage.write(anchor.clone()).unwrap();
anchor
});
let origin = "https://example.com".to_string();
let anchor_number = anchor.anchor_number();

Expand Down Expand Up @@ -944,7 +980,11 @@ fn can_get_default_before_update_account_for_origin() {
use crate::state::{storage_borrow_mut, storage_replace};

storage_replace(storage_with_salt());
let anchor = storage_borrow_mut(|storage| storage.allocate_anchor(0).unwrap());
let anchor = storage_borrow_mut(|storage| {
let anchor = storage.allocate_anchor(0).unwrap();
storage.write(anchor.clone()).unwrap();
anchor
});
let origin = "https://example.com".to_string();
let anchor_number = anchor.anchor_number();

Expand All @@ -962,7 +1002,11 @@ fn should_get_updated_default_account_after_modification() {
use crate::state::{storage_borrow_mut, storage_replace};

storage_replace(storage_with_salt());
let anchor = storage_borrow_mut(|storage| storage.allocate_anchor(0).unwrap());
let anchor = storage_borrow_mut(|storage| {
let anchor = storage.allocate_anchor(0).unwrap();
storage.write(anchor.clone()).unwrap();
anchor
});
let origin = "https://example.com".to_string();
let anchor_number = anchor.anchor_number();

Expand Down Expand Up @@ -1029,7 +1073,11 @@ fn should_fall_back_to_the_tracked_default_when_the_reservation_is_stale() {
// to be there for the same test to hold all the way up the stack.
storage.update_salt([17u8; 32]);
storage_replace(storage);
let anchor = storage_borrow_mut(|storage| storage.allocate_anchor(0).unwrap());
let anchor = storage_borrow_mut(|storage| {
let anchor = storage.allocate_anchor(0).unwrap();
storage.write(anchor.clone()).unwrap();
anchor
});
let anchor_number = anchor.anchor_number();
let origin = "https://example.com".to_string();
storage_borrow_mut(|storage| storage.write(anchor)).unwrap();
Expand Down Expand Up @@ -1063,7 +1111,11 @@ fn should_get_default_account_for_different_origins() {
use crate::state::{storage_borrow_mut, storage_replace};

storage_replace(storage_with_salt());
let anchor = storage_borrow_mut(|storage| storage.allocate_anchor(0).unwrap());
let anchor = storage_borrow_mut(|storage| {
let anchor = storage.allocate_anchor(0).unwrap();
storage.write(anchor.clone()).unwrap();
anchor
});
let origin1 = "https://app1.com".to_string();
let origin2 = "https://app2.com".to_string();
let anchor_number = anchor.anchor_number();
Expand Down
28 changes: 28 additions & 0 deletions src/internet_identity/src/delegation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,34 @@ pub fn calculate_account_seed_with_salt(
hash_bytes(blob)
}

const SESSION_SEED_PREFIX: &str = "session";

/// The seed of a session's canister-signed identity.
///
/// Built on the account's own seed, so a session survives anything that leaves the
/// account's principal unchanged, including naming a default account. `session_id` is
/// the only other input, and no two sessions are ever allocated the same one, so a
/// revoked session's identity can never be arrived at a second time. Unguessability
/// comes from the salt.
pub fn calculate_session_seed_with_salt(
salt: &[u8; 32],
account_seed: &Hash,
session_id: SessionId,
) -> Hash {
fn push_field(blob: &mut Vec<u8>, data: &[u8]) {
blob.extend_from_slice(&(data.len() as u64).to_be_bytes());
blob.extend_from_slice(data);
}

let mut blob: Vec<u8> = vec![];
push_field(&mut blob, salt);
push_field(&mut blob, SESSION_SEED_PREFIX.as_bytes());
push_field(&mut blob, account_seed);
push_field(&mut blob, &session_id.to_be_bytes());

hash_bytes(blob)
}

fn hash_bytes(value: impl AsRef<[u8]>) -> Hash {
let mut hasher = Sha256::new();
hasher.update(value.as_ref());
Expand Down
1 change: 1 addition & 0 deletions src/internet_identity/src/email_recovery/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ mod tests {
let mut a = Anchor {
browsers: vec![],
next_browser_id: 0,
session_count: 0,
anchor_number: 1,
devices: vec![],
openid_credentials: vec![],
Expand Down
11 changes: 6 additions & 5 deletions src/internet_identity/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1202,11 +1202,12 @@ mod v2_api {
let stored_browsers: Vec<BrowserInfo> = state::anchor(identity_number)
.browsers()
.iter()
.map(|device| BrowserInfo {
id: device.id,
name: device.name.clone(),
created_at: device.created_at,
last_used: device.last_used,
.map(|browser| BrowserInfo {
id: browser.id,
name: browser.name.clone(),
created_at: browser.created_at,
last_used: browser.last_used,
session_count: browser.session_count,
})
.collect();
let browsers = if stored_browsers.is_empty() {
Expand Down
Loading
Loading