Public Access
Merge branch 'main' into the vaults screen, and let it rotate keys too
Main built vault key rotation while this branch was reshaping the screen that would drive it, so the two met in the same three files. Every other conflict was textual and resolved by taking both; these are the ones where a decision had to be made. **The view model.** Main taught TeamsViewModel three things and this branch had renamed and rewritten it into VaultsViewModel. All three are ported rather than dropped, because each is a behaviour rather than wording: adding somebody now wraps the vault to them on the spot instead of leaving SHARE KEY to be pressed, removing somebody rotates the vault and hands the new key to whoever is left, and a share reports how many generations were wrapped. The session calls they reach — ShareTeamVaultsAsync and RekeyTeamVaultsAsync — are scoped to a membership list rather than to one vault, and they are called that way here rather than narrowed: adding somebody is a change to the list, so every vault the list carries is one they can now fetch. This screen makes lists that carry one vault, so the sentences name one; where a list carries several, naming them all is the honest report, and the members section already says the list is shared. AddMemberAsync ran two lines over the length limit once the sharing was in it, so the calls behind it moved to AddOrInviteAsync and the three-way refusal to WhyNobodyCanBeAdded — the command reads as its guards now, which is what it was before the sharing arrived. **The tests.** Main's four new cases are ported to the vault-first API, including the one that matters most: the tampered key log is corrupted *before* the add, because the add is now a route to a wrap and a test that corrupted it afterwards would be asserting about the manual route only. SelectingAVault_ListsWhoHoldsAKey now expects two holders rather than one — main's fake records the creator's own self-grant, and a key-holder list that omitted it would show the one person who can certainly open a new vault as somebody who cannot. **The README.** The limits list is six rather than four or five: main's rotation entries and this branch's "a vault cannot be deleted" describe different things and both are true. "The rekey is flagged, never performed" is gone, since it is now performed, and M3 reads *Done* rather than *Done, except rekey*. One thing worth writing down that neither side had. An invitation claimed at sign-in still leaves the key owed, where an add does not: at the moment an invitation is issued there is no account and no published key to wrap to, and the claim happens on the invitee's machine, which holds nothing. Manual check 12.1 says so, because a reader who knows adding shares would otherwise read that step as stale. 1561 tests pass.
This commit is contained in:
@@ -55,6 +55,7 @@ namespace DodoSSH.Contracts;
|
||||
[JsonSerializable(typeof(CreateTeamVaultRequest))]
|
||||
[JsonSerializable(typeof(UpdateVaultRequest))]
|
||||
[JsonSerializable(typeof(IssueVaultGrantRequest))]
|
||||
[JsonSerializable(typeof(RekeyVaultRequest))]
|
||||
[JsonSerializable(typeof(VaultGrantsResponse))]
|
||||
[JsonSerializable(typeof(KeyLogPage))]
|
||||
[JsonSerializable(typeof(SyncPullRequest))]
|
||||
|
||||
@@ -209,6 +209,16 @@ public sealed record MeResponse(
|
||||
/// must complete it.
|
||||
/// </param>
|
||||
/// <param name="RekeyRequired">Whether a membership change has left this vault needing a rekey.</param>
|
||||
/// <param name="PriorKeyWraps">
|
||||
/// Generations before <paramref name="KeyGeneration"/> that this caller still holds a grant for.
|
||||
/// <para>
|
||||
/// Empty for a vault that has never been rotated, which is nearly all of them. It is not empty after
|
||||
/// one, and it has to be served: an item is sealed under the generation in force when it was written,
|
||||
/// so a client that held only the current key would find every item older than the rotation
|
||||
/// undecryptable. See <c>VaultGrantService.RekeyAsync</c> for why old grants are kept rather than
|
||||
/// revoked.
|
||||
/// </para>
|
||||
/// </param>
|
||||
public sealed record VaultSummary(
|
||||
Guid VaultId,
|
||||
string Name,
|
||||
@@ -217,4 +227,14 @@ public sealed record VaultSummary(
|
||||
uint KeyGeneration,
|
||||
int Permissions,
|
||||
byte[]? WrappedVaultKey,
|
||||
bool RekeyRequired);
|
||||
bool RekeyRequired,
|
||||
IReadOnlyList<VaultKeyWrap>? PriorKeyWraps = null);
|
||||
|
||||
/// <summary>A vault key sealed to one recipient, at one generation.</summary>
|
||||
/// <remarks>
|
||||
/// Only ever the caller's own. <c>VaultGrantSummary</c> deliberately carries no wrap: serving every
|
||||
/// member's sealed key to every member would widen what a stolen access token yields for nothing.
|
||||
/// </remarks>
|
||||
/// <param name="KeyGeneration">The generation this wrap opens.</param>
|
||||
/// <param name="WrappedKey">The vault key sealed to the caller's X25519 key. Opaque.</param>
|
||||
public sealed record VaultKeyWrap(uint KeyGeneration, byte[] WrappedKey);
|
||||
|
||||
@@ -359,6 +359,19 @@ DodoSSH.Contracts.RegisterDeviceResponse.EnrolledAt.get -> System.DateTimeOffset
|
||||
DodoSSH.Contracts.RegisterDeviceResponse.EnrolledAt.init -> void
|
||||
DodoSSH.Contracts.RegisterDeviceResponse.Equals(DodoSSH.Contracts.RegisterDeviceResponse? other) -> bool
|
||||
DodoSSH.Contracts.RegisterDeviceResponse.RegisterDeviceResponse(System.Guid DeviceId, System.DateTimeOffset EnrolledAt) -> void
|
||||
DodoSSH.Contracts.RekeyVaultRequest
|
||||
DodoSSH.Contracts.RekeyVaultRequest.<Clone>$() -> DodoSSH.Contracts.RekeyVaultRequest!
|
||||
DodoSSH.Contracts.RekeyVaultRequest.Deconstruct(out uint KeyGeneration, out byte[]! WrappedVaultKey, out byte[]! GrantSignature, out System.DateTimeOffset GrantedAt) -> void
|
||||
DodoSSH.Contracts.RekeyVaultRequest.Equals(DodoSSH.Contracts.RekeyVaultRequest? other) -> bool
|
||||
DodoSSH.Contracts.RekeyVaultRequest.GrantedAt.get -> System.DateTimeOffset
|
||||
DodoSSH.Contracts.RekeyVaultRequest.GrantedAt.init -> void
|
||||
DodoSSH.Contracts.RekeyVaultRequest.GrantSignature.get -> byte[]!
|
||||
DodoSSH.Contracts.RekeyVaultRequest.GrantSignature.init -> void
|
||||
DodoSSH.Contracts.RekeyVaultRequest.KeyGeneration.get -> uint
|
||||
DodoSSH.Contracts.RekeyVaultRequest.KeyGeneration.init -> void
|
||||
DodoSSH.Contracts.RekeyVaultRequest.RekeyVaultRequest(uint KeyGeneration, byte[]! WrappedVaultKey, byte[]! GrantSignature, System.DateTimeOffset GrantedAt) -> void
|
||||
DodoSSH.Contracts.RekeyVaultRequest.WrappedVaultKey.get -> byte[]!
|
||||
DodoSSH.Contracts.RekeyVaultRequest.WrappedVaultKey.init -> void
|
||||
DodoSSH.Contracts.RelayConfiguration
|
||||
DodoSSH.Contracts.RelayConfiguration.<Clone>$() -> DodoSSH.Contracts.RelayConfiguration!
|
||||
DodoSSH.Contracts.RelayConfiguration.Deconstruct(out bool Enabled, out System.Uri? WebSocketUrl) -> void
|
||||
@@ -712,9 +725,18 @@ DodoSSH.Contracts.VaultGrantSummary.RevokedAt.init -> void
|
||||
DodoSSH.Contracts.VaultGrantSummary.State.get -> DodoSSH.Contracts.VaultGrantState
|
||||
DodoSSH.Contracts.VaultGrantSummary.State.init -> void
|
||||
DodoSSH.Contracts.VaultGrantSummary.VaultGrantSummary(System.Guid RecipientUserId, string? Email, string? DisplayName, uint KeyGeneration, DodoSSH.Contracts.VaultGrantState State, System.Guid GranterUserId, System.DateTimeOffset CreatedAt, System.DateTimeOffset? RevokedAt) -> void
|
||||
DodoSSH.Contracts.VaultKeyWrap
|
||||
DodoSSH.Contracts.VaultKeyWrap.<Clone>$() -> DodoSSH.Contracts.VaultKeyWrap!
|
||||
DodoSSH.Contracts.VaultKeyWrap.Deconstruct(out uint KeyGeneration, out byte[]! WrappedKey) -> void
|
||||
DodoSSH.Contracts.VaultKeyWrap.Equals(DodoSSH.Contracts.VaultKeyWrap? other) -> bool
|
||||
DodoSSH.Contracts.VaultKeyWrap.KeyGeneration.get -> uint
|
||||
DodoSSH.Contracts.VaultKeyWrap.KeyGeneration.init -> void
|
||||
DodoSSH.Contracts.VaultKeyWrap.VaultKeyWrap(uint KeyGeneration, byte[]! WrappedKey) -> void
|
||||
DodoSSH.Contracts.VaultKeyWrap.WrappedKey.get -> byte[]!
|
||||
DodoSSH.Contracts.VaultKeyWrap.WrappedKey.init -> void
|
||||
DodoSSH.Contracts.VaultSummary
|
||||
DodoSSH.Contracts.VaultSummary.<Clone>$() -> DodoSSH.Contracts.VaultSummary!
|
||||
DodoSSH.Contracts.VaultSummary.Deconstruct(out System.Guid VaultId, out string! Name, out bool IsPersonal, out System.Guid? TeamId, out uint KeyGeneration, out int Permissions, out byte[]? WrappedVaultKey, out bool RekeyRequired) -> void
|
||||
DodoSSH.Contracts.VaultSummary.Deconstruct(out System.Guid VaultId, out string! Name, out bool IsPersonal, out System.Guid? TeamId, out uint KeyGeneration, out int Permissions, out byte[]? WrappedVaultKey, out bool RekeyRequired, out System.Collections.Generic.IReadOnlyList<DodoSSH.Contracts.VaultKeyWrap!>? PriorKeyWraps) -> void
|
||||
DodoSSH.Contracts.VaultSummary.Equals(DodoSSH.Contracts.VaultSummary? other) -> bool
|
||||
DodoSSH.Contracts.VaultSummary.IsPersonal.get -> bool
|
||||
DodoSSH.Contracts.VaultSummary.IsPersonal.init -> void
|
||||
@@ -724,13 +746,15 @@ DodoSSH.Contracts.VaultSummary.Name.get -> string!
|
||||
DodoSSH.Contracts.VaultSummary.Name.init -> void
|
||||
DodoSSH.Contracts.VaultSummary.Permissions.get -> int
|
||||
DodoSSH.Contracts.VaultSummary.Permissions.init -> void
|
||||
DodoSSH.Contracts.VaultSummary.PriorKeyWraps.get -> System.Collections.Generic.IReadOnlyList<DodoSSH.Contracts.VaultKeyWrap!>?
|
||||
DodoSSH.Contracts.VaultSummary.PriorKeyWraps.init -> void
|
||||
DodoSSH.Contracts.VaultSummary.RekeyRequired.get -> bool
|
||||
DodoSSH.Contracts.VaultSummary.RekeyRequired.init -> void
|
||||
DodoSSH.Contracts.VaultSummary.TeamId.get -> System.Guid?
|
||||
DodoSSH.Contracts.VaultSummary.TeamId.init -> void
|
||||
DodoSSH.Contracts.VaultSummary.VaultId.get -> System.Guid
|
||||
DodoSSH.Contracts.VaultSummary.VaultId.init -> void
|
||||
DodoSSH.Contracts.VaultSummary.VaultSummary(System.Guid VaultId, string! Name, bool IsPersonal, System.Guid? TeamId, uint KeyGeneration, int Permissions, byte[]? WrappedVaultKey, bool RekeyRequired) -> void
|
||||
DodoSSH.Contracts.VaultSummary.VaultSummary(System.Guid VaultId, string! Name, bool IsPersonal, System.Guid? TeamId, uint KeyGeneration, int Permissions, byte[]? WrappedVaultKey, bool RekeyRequired, System.Collections.Generic.IReadOnlyList<DodoSSH.Contracts.VaultKeyWrap!>? PriorKeyWraps = null) -> void
|
||||
DodoSSH.Contracts.VaultSummary.WrappedVaultKey.get -> byte[]?
|
||||
DodoSSH.Contracts.VaultSummary.WrappedVaultKey.init -> void
|
||||
override DodoSSH.Contracts.AddTeamMemberRequest.Equals(object? obj) -> bool
|
||||
@@ -796,6 +820,9 @@ override DodoSSH.Contracts.RegisterDeviceRequest.ToString() -> string!
|
||||
override DodoSSH.Contracts.RegisterDeviceResponse.Equals(object? obj) -> bool
|
||||
override DodoSSH.Contracts.RegisterDeviceResponse.GetHashCode() -> int
|
||||
override DodoSSH.Contracts.RegisterDeviceResponse.ToString() -> string!
|
||||
override DodoSSH.Contracts.RekeyVaultRequest.Equals(object? obj) -> bool
|
||||
override DodoSSH.Contracts.RekeyVaultRequest.GetHashCode() -> int
|
||||
override DodoSSH.Contracts.RekeyVaultRequest.ToString() -> string!
|
||||
override DodoSSH.Contracts.RelayConfiguration.Equals(object? obj) -> bool
|
||||
override DodoSSH.Contracts.RelayConfiguration.GetHashCode() -> int
|
||||
override DodoSSH.Contracts.RelayConfiguration.ToString() -> string!
|
||||
@@ -856,6 +883,9 @@ override DodoSSH.Contracts.VaultGrantsResponse.ToString() -> string!
|
||||
override DodoSSH.Contracts.VaultGrantSummary.Equals(object? obj) -> bool
|
||||
override DodoSSH.Contracts.VaultGrantSummary.GetHashCode() -> int
|
||||
override DodoSSH.Contracts.VaultGrantSummary.ToString() -> string!
|
||||
override DodoSSH.Contracts.VaultKeyWrap.Equals(object? obj) -> bool
|
||||
override DodoSSH.Contracts.VaultKeyWrap.GetHashCode() -> int
|
||||
override DodoSSH.Contracts.VaultKeyWrap.ToString() -> string!
|
||||
override DodoSSH.Contracts.VaultSummary.Equals(object? obj) -> bool
|
||||
override DodoSSH.Contracts.VaultSummary.GetHashCode() -> int
|
||||
override DodoSSH.Contracts.VaultSummary.ToString() -> string!
|
||||
@@ -904,6 +934,8 @@ static DodoSSH.Contracts.RegisterDeviceRequest.operator !=(DodoSSH.Contracts.Reg
|
||||
static DodoSSH.Contracts.RegisterDeviceRequest.operator ==(DodoSSH.Contracts.RegisterDeviceRequest? left, DodoSSH.Contracts.RegisterDeviceRequest? right) -> bool
|
||||
static DodoSSH.Contracts.RegisterDeviceResponse.operator !=(DodoSSH.Contracts.RegisterDeviceResponse? left, DodoSSH.Contracts.RegisterDeviceResponse? right) -> bool
|
||||
static DodoSSH.Contracts.RegisterDeviceResponse.operator ==(DodoSSH.Contracts.RegisterDeviceResponse? left, DodoSSH.Contracts.RegisterDeviceResponse? right) -> bool
|
||||
static DodoSSH.Contracts.RekeyVaultRequest.operator !=(DodoSSH.Contracts.RekeyVaultRequest? left, DodoSSH.Contracts.RekeyVaultRequest? right) -> bool
|
||||
static DodoSSH.Contracts.RekeyVaultRequest.operator ==(DodoSSH.Contracts.RekeyVaultRequest? left, DodoSSH.Contracts.RekeyVaultRequest? right) -> bool
|
||||
static DodoSSH.Contracts.RelayConfiguration.operator !=(DodoSSH.Contracts.RelayConfiguration? left, DodoSSH.Contracts.RelayConfiguration? right) -> bool
|
||||
static DodoSSH.Contracts.RelayConfiguration.operator ==(DodoSSH.Contracts.RelayConfiguration? left, DodoSSH.Contracts.RelayConfiguration? right) -> bool
|
||||
static DodoSSH.Contracts.RelaySessionSummary.operator !=(DodoSSH.Contracts.RelaySessionSummary? left, DodoSSH.Contracts.RelaySessionSummary? right) -> bool
|
||||
@@ -944,5 +976,7 @@ static DodoSSH.Contracts.VaultGrantsResponse.operator !=(DodoSSH.Contracts.Vault
|
||||
static DodoSSH.Contracts.VaultGrantsResponse.operator ==(DodoSSH.Contracts.VaultGrantsResponse? left, DodoSSH.Contracts.VaultGrantsResponse? right) -> bool
|
||||
static DodoSSH.Contracts.VaultGrantSummary.operator !=(DodoSSH.Contracts.VaultGrantSummary? left, DodoSSH.Contracts.VaultGrantSummary? right) -> bool
|
||||
static DodoSSH.Contracts.VaultGrantSummary.operator ==(DodoSSH.Contracts.VaultGrantSummary? left, DodoSSH.Contracts.VaultGrantSummary? right) -> bool
|
||||
static DodoSSH.Contracts.VaultKeyWrap.operator !=(DodoSSH.Contracts.VaultKeyWrap? left, DodoSSH.Contracts.VaultKeyWrap? right) -> bool
|
||||
static DodoSSH.Contracts.VaultKeyWrap.operator ==(DodoSSH.Contracts.VaultKeyWrap? left, DodoSSH.Contracts.VaultKeyWrap? right) -> bool
|
||||
static DodoSSH.Contracts.VaultSummary.operator !=(DodoSSH.Contracts.VaultSummary? left, DodoSSH.Contracts.VaultSummary? right) -> bool
|
||||
static DodoSSH.Contracts.VaultSummary.operator ==(DodoSSH.Contracts.VaultSummary? left, DodoSSH.Contracts.VaultSummary? right) -> bool
|
||||
|
||||
@@ -425,6 +425,37 @@ public sealed record IssueVaultGrantRequest(
|
||||
byte[] GrantSignature,
|
||||
DateTimeOffset GrantedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Moves a vault to a fresh key, wrapped to the caller.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The new key is generated by a client that already holds the current one, and arrives sealed to that
|
||||
/// same client — the server can neither produce it nor tell that it differs from the old one. What the
|
||||
/// server does is decide the moment it takes effect: the generation advances in one transaction, so
|
||||
/// there is no instant at which two clients disagree about which generation is current.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Grants for earlier generations are kept, not revoked.</b> Every item still carries the generation
|
||||
/// it was sealed under, so withdrawing them would make the vault's whole history unreadable to the
|
||||
/// people who are still in it. The departed member's grants are revoked — that is what
|
||||
/// <c>RevokeGrantAsync</c> and removal from the team already do — and this is what stops them reading
|
||||
/// anything written from here on. It does not reach back; see ADR 0001.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="KeyGeneration">
|
||||
/// The generation being created. Must be exactly one past the vault's current one, so two clients
|
||||
/// rotating at once cannot both believe they succeeded.
|
||||
/// </param>
|
||||
/// <param name="WrappedVaultKey">The new vault key, sealed to the caller's own encryption key.</param>
|
||||
/// <param name="GrantSignature">Ed25519 signature over the canonical grant tuple.</param>
|
||||
/// <param name="GrantedAt">Signing timestamp, part of the signed tuple.</param>
|
||||
public sealed record RekeyVaultRequest(
|
||||
uint KeyGeneration,
|
||||
byte[] WrappedVaultKey,
|
||||
byte[] GrantSignature,
|
||||
DateTimeOffset GrantedAt);
|
||||
|
||||
/// <summary>One vault key grant, as the sharing interface sees it.</summary>
|
||||
/// <remarks>
|
||||
/// The wrapped key itself is deliberately not here. A member reads their own through
|
||||
@@ -457,7 +488,12 @@ public sealed record VaultGrantSummary(
|
||||
/// compares against rather than inferring from <see cref="VaultGrantSummary.State"/> alone.
|
||||
/// </param>
|
||||
/// <param name="RekeyRequired">Whether a membership change has left this vault needing a rekey.</param>
|
||||
/// <param name="Grants">Every grant, including revoked ones.</param>
|
||||
/// <param name="Grants">
|
||||
/// One row per holder, including those whose access has been withdrawn. Not one per grant: a rotated
|
||||
/// vault leaves a member holding one grant per generation, and the row carries the best of them — so
|
||||
/// <see cref="VaultGrantSummary.KeyGeneration"/> below <paramref name="KeyGeneration"/> means they have
|
||||
/// not been wrapped the current key yet, rather than that one of their grants is old.
|
||||
/// </param>
|
||||
public sealed record VaultGrantsResponse(
|
||||
Guid VaultId,
|
||||
uint KeyGeneration,
|
||||
|
||||
Reference in New Issue
Block a user