using System.Diagnostics.CodeAnalysis; namespace DodoSSH.Client.Domain; /// /// A Spelunk API key and the one Spelunk it may be sent to, decrypted. /// /// /// /// The address is what makes this an item of its own. A key kept as a credential's password can be /// sent anywhere a host names, and a link from Spelunk can be sent by anything on the machine — so a link /// that is not already backed by a saved host has to stop and ask. A key stored with its address says, once /// and in the keychain, where the user agreed it may go: a link from that Spelunk connects straight away, and /// a host naming this key with any other address is refused at connect rather than sending it there. See /// VaultViewModel.OpenSpelunkLinkAsync. /// /// /// is a password, and everything this codebase does about passwords applies to /// it, as it does to : it is inside the encrypted payload, /// the activity log records that it changed and never what it was, and the merge reports that it differed /// rather than either value. /// /// public sealed record SpelunkKeySecret : IVaultSecret { /// The longest API key stored. /// /// Far beyond any real key, for the reason exists: an /// unbounded string is the shape a corrupted payload takes, and this one goes into a request header. /// public const int MaxApiKeyLength = 4096; /// What the user calls this key. public required string Label { get; init; } /// /// The Spelunk API this key is for, as writes it, and the /// only address it is ever sent to. /// /// /// Normalised for the reason is: it is compared, byte for byte, with /// the address a host or a link names. /// public required string ApiBase { get; init; } /// The API key itself, as Spelunk issued it. public required string ApiKey { get; init; } /// Whether this is storable, and why not if it is not. public bool TryValidate([NotNullWhen(false)] out string? reason) { if (string.IsNullOrWhiteSpace(Label)) { reason = "A Spelunk key needs a name."; return false; } if (string.IsNullOrWhiteSpace(ApiBase)) { reason = "A Spelunk key needs the address of the Spelunk it is for."; return false; } if (!SpelunkRoute.TryNormalizeApiBase(ApiBase, out var normalized, out reason)) { return false; } if (!string.Equals(normalized, ApiBase, StringComparison.Ordinal)) { reason = "The Spelunk API address is not in its normal form."; return false; } if (string.IsNullOrWhiteSpace(ApiKey)) { reason = "A Spelunk key needs the key."; return false; } // A header value cannot hold a line break, and a key with one would fail at the first connect with an // HTTP error that names neither the key nor this item. if (ApiKey.Length > MaxApiKeyLength || ApiKey.Any(char.IsControl)) { reason = "That does not look like a Spelunk API key."; return false; } reason = null; return true; } }