namespace DodoSSH.Client.Session; /// /// How far withdrawing a device key got. /// /// /// Three outcomes rather than a boolean, because the middle one is a state the user has to be told about /// and can act on. The local half always happens; only the server half can be out of reach. /// public enum DeviceRevocation { /// There was no device key here to withdraw. NothingRegistered, /// /// This machine can no longer unlock itself, but the account still lists the device. /// /// /// Offline. The wrap on the server is the user's bundle sealed to a key this machine has now destroyed, /// so nothing can open it — but it is still a row that should not be there, and a reinstall would pull /// it down again and offer a device unlock that cannot work. /// LocalOnly, /// Gone from this machine and from the account. Complete, } /// /// Where this machine keeps the private half of its device key. /// /// /// /// An interface because the answer is a platform decision and a security decision, recorded in /// ADR 0007: on Windows a gesture guards it, and the gesture is the whole value — it is what stops a /// process running as the user from unlocking the vault silently. Nothing in this assembly should know /// which gesture, and nothing above it should be able to skip one. /// /// /// Exactly 32 bytes, and only because the cache key moved. This holds the X25519 device private /// key and nothing else. It would have had to hold the local cache key too — a second live secret at /// rest, going stale on every passphrase change — had that key not been re-keyed to the identity bundle /// first. See docs/crypto.md §3.2. /// /// /// Every member may fail or refuse, and refusal is ordinary rather than exceptional: a user can cancel a /// fingerprint prompt, a Hello key is invalidated when the PIN is reset, and a machine may have no /// keystore at all. therefore returns null rather than throwing, and the /// caller's answer is always the same — ask for the passphrase. /// /// public interface IDeviceKeyStore { /// Whether this machine can keep a device key at all. /// /// Asked before offering to register one. Registering a device whose private half does not survive /// the process would put a wrap on the server that nothing can ever open, and make the account's /// device list claim a capability this machine does not have. /// ValueTask IsAvailableAsync(CancellationToken cancellationToken); /// Stores the device private key, replacing any already held. /// The raw 32-byte X25519 scalar. /// Cancellation token. ValueTask SaveAsync(ReadOnlyMemory devicePrivateKey, CancellationToken cancellationToken); /// /// Retrieves the device private key, prompting for whatever guards it. /// /// /// The raw scalar, or if there is none, the user declined, or the platform /// has invalidated it. The three are deliberately not distinguished: the caller does the same thing /// in each case, and a message naming which one would be describing the keystore rather than telling /// the user anything they can act on. /// ValueTask TryLoadAsync(CancellationToken cancellationToken); /// Discards the stored key. /// /// Local only. The server's wrap row outlives this and has to be deleted separately, or the account /// will go on listing a device that can no longer unlock anything. /// ValueTask ForgetAsync(CancellationToken cancellationToken); } /// /// A machine with nowhere to keep a device key. /// /// /// What the application composes until a real keystore is wired up, and the honest answer for a platform /// that has none. Reports unavailable and holds nothing, so unlock asks for the passphrase exactly as it /// did before any of this existed — a placeholder that changes no behaviour rather than one that pretends. /// public sealed class UnavailableDeviceKeyStore : IDeviceKeyStore { /// public ValueTask IsAvailableAsync(CancellationToken cancellationToken) => ValueTask.FromResult(false); /// public ValueTask SaveAsync(ReadOnlyMemory devicePrivateKey, CancellationToken cancellationToken) => throw new NotSupportedException( "This machine has no device key store. Check IsAvailableAsync before offering to register one."); /// public ValueTask TryLoadAsync(CancellationToken cancellationToken) => ValueTask.FromResult(null); /// public ValueTask ForgetAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask; }