Public Access
Fill in the Spelunk API key on the Security page, as a synced Spelunk key that goes only to its own address
Settings → Security gains a SPELUNK section: an API address and a key, saved as a new keychain item, a Spelunk key (SyncEntityType 14, AadResourceType 17, server table spelunk_key via the AddSpelunkKeyItem migration). It is ciphertext only on the server, lives in the personal vault, syncs like every secret, and holds the key together with the one address it may be sent to. The key box empties once saved; the list shows name, address and how many hosts use each key, never the key. Saving a key for an address that already has one replaces it, and deleting goes through the shared confirmation card with the hosts that would stop connecting. A host's route names a saved key where it named a credential, in the same apiKeyCredentialId field, so no host schema changes and an older build reads such a host as one whose key credential has gone. At connect a saved key is sent only to its own address; a host naming it with another Spelunk address is refused in words. Keys kept as a credential's password still work. The editor offers saved keys first and picks the one saved for an address as it is typed. A link from a Spelunk with a saved key now connects straight away, including the first one: saving the key is the user's say-so that the first-link editor used to ask for. The address and key merge as one pair, so two concurrent edits can never pair one device's key with another's address, and the key never reaches the conflict log.
This commit is contained in:
1 parent
ee423b25ce
commit
b08f2c58e3
37 files changed
+3907
-64
No files matched your search
@@ -62,11 +62,11 @@ internal sealed partial class VaultViewModel
|
||||
[ObservableProperty]
|
||||
private string editorSpelunkServerId = string.Empty;
|
||||
|
||||
/// <summary>The credentials the editor offers as the Spelunk API key.</summary>
|
||||
/// <summary>What the editor offers as the Spelunk API key: saved Spelunk keys, then credentials.</summary>
|
||||
/// <remarks>
|
||||
/// Credentials only, and no "none" entry: a route with no key cannot be opened, and nothing selected is
|
||||
/// how a host made from a link says it has not been chosen yet. A key the host names that has since been
|
||||
/// deleted keeps a placeholder, for the reason <see cref="BuildAuthenticationChoices"/> keeps one.
|
||||
/// No "none" entry: a route with no key cannot be opened, and nothing selected is how a host made from a
|
||||
/// link says it has not been chosen yet. A key the host names that has since been deleted keeps a
|
||||
/// placeholder, for the reason <see cref="BuildAuthenticationChoices"/> keeps one.
|
||||
/// </remarks>
|
||||
internal ObservableCollection<AuthenticationChoice> EditorSpelunkKeyChoices { get; } = [];
|
||||
|
||||
@@ -170,13 +170,13 @@ internal sealed partial class VaultViewModel
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a Spelunk tunnel to a host, with the key its credential holds.
|
||||
/// Opens a Spelunk tunnel to a host, with the key it names.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The key is read from the keychain at the moment of connecting, like every other secret the connect
|
||||
/// path uses, and goes only to the API address the host names. A credential that has gone is refused in
|
||||
/// words, exactly as a missing SSH credential is — see <see cref="TryBuildAuthentication"/>.
|
||||
/// path uses, and goes only to the API address the host names — and for a saved Spelunk key, only when
|
||||
/// that is the address it was saved for. See <see cref="SpelunkApiKeyFor"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The port is the host's resolved one, group and all: it is the port the agent connects to on the
|
||||
@@ -194,18 +194,13 @@ internal sealed partial class VaultViewModel
|
||||
SpelunkRoute route,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (Credentials.FirstOrDefault(credential => credential.EntityId == route.ApiKeyCredentialId) is not { } key)
|
||||
{
|
||||
throw new SpelunkTunnelException(
|
||||
$"'{row.Label}' is reached through Spelunk with an API key credential that is not in this "
|
||||
+ "keychain any more. Edit the host to choose another one.");
|
||||
}
|
||||
var apiKey = SpelunkApiKeyFor(row, route);
|
||||
|
||||
return await Spelunk
|
||||
.OpenAsync(
|
||||
route.ApiBase,
|
||||
route.ServerId,
|
||||
key.Credential.Password,
|
||||
apiKey,
|
||||
row.Resolved.Port.Value,
|
||||
route.SignInThroughSpelunk ? SpelunkSignIn.Create() : null,
|
||||
cancellationToken)
|
||||
@@ -227,16 +222,17 @@ internal sealed partial class VaultViewModel
|
||||
/// <para>
|
||||
/// <b>Otherwise, when that Spelunk already gets an API key, the host is made and connected to.</b> In the
|
||||
/// personal vault, tagged, named as the link names it, and signing in through Spelunk — so it needs no
|
||||
/// SSH key or password — with the key credential a saved host already sends to that same Spelunk. It is
|
||||
/// saved the ordinary way and syncs like any host. Nothing leaks by this: the key goes where it already
|
||||
/// goes, and the server is one that key's own organisation holds or Spelunk refuses it.
|
||||
/// SSH key or password — with the key saved for that Spelunk under Settings → Security, or else the key
|
||||
/// credential a saved host already sends there. It is saved the ordinary way and syncs like any host.
|
||||
/// Nothing leaks by this: the key goes where the user already said it goes, and the server is one that
|
||||
/// key's own organisation holds or Spelunk refuses it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The first link from a Spelunk opens the editor instead, and nothing more.</b> Anything on this
|
||||
/// machine can make a link, so it must never be able to send an API key to an address no saved host
|
||||
/// already sends it to. The host is pre-filled — signing in through Spelunk, tagged, in the personal
|
||||
/// vault — and the user chooses the credential holding the key and saves; every later link from that
|
||||
/// Spelunk connects straight away.
|
||||
/// <b>The first link from a Spelunk with no key opens the editor instead, and nothing more.</b> Anything
|
||||
/// on this machine can make a link, so it must never be able to send an API key to an address the user
|
||||
/// has not sent it to. The host is pre-filled — signing in through Spelunk, tagged, in the personal
|
||||
/// vault — and the user chooses the key and saves; every later link from that Spelunk connects straight
|
||||
/// away.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal async Task OpenSpelunkLinkAsync(SpelunkLink link)
|
||||
@@ -278,20 +274,29 @@ internal sealed partial class VaultViewModel
|
||||
string.Equals(tag.Label.Trim(), SpelunkTag, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// The credential a saved host already sends to this Spelunk as its API key, or null when none does.
|
||||
/// The key the user has already agreed may go to this Spelunk, or null when there is none.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The personal vault's hosts first, so the answer does not depend on the order vaults happen to load in;
|
||||
/// and only a credential still in the keychain, because a dangling one would make a host that cannot
|
||||
/// connect.
|
||||
/// <para>
|
||||
/// A Spelunk key saved for that address first — it is the one thing that says so in as many words — and
|
||||
/// otherwise the key a saved host already sends there, which is how the answer was reached before saved
|
||||
/// keys existed and is still true of every host set up that way.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The personal vault's first, so the answer does not depend on the order vaults happen to load in; and
|
||||
/// only a key still in the keychain, because a dangling one would make a host that cannot connect.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private Guid? KnownSpelunkKey(string apiBase, Guid? personal) =>
|
||||
Hosts
|
||||
SavedSpelunkKeyFor(apiBase, personal)?.EntityId
|
||||
?? Hosts
|
||||
.OrderByDescending(row => row.VaultId == personal)
|
||||
.Select(row => row.Host.Spelunk)
|
||||
.Where(route => route is not null && string.Equals(route.ApiBase, apiBase, StringComparison.Ordinal))
|
||||
.Select(route => (Guid?)route!.ApiKeyCredentialId)
|
||||
.FirstOrDefault(id => Credentials.Any(credential => credential.EntityId == id));
|
||||
.FirstOrDefault(id =>
|
||||
Credentials.Any(credential => credential.EntityId == id)
|
||||
|| SpelunkKeys.Any(key => key.EntityId == id && string.Equals(key.ApiBase, apiBase, StringComparison.Ordinal)));
|
||||
|
||||
/// <summary>The host a link makes: what the link says, signing in through Spelunk with a known key.</summary>
|
||||
private static HostSecret HostFromLink(SpelunkLink link, Guid apiKeyCredentialId, TagSet tags) => new()
|
||||
@@ -390,9 +395,9 @@ internal sealed partial class VaultViewModel
|
||||
IsHostPaneOpen = true;
|
||||
|
||||
var said = EditorSelectedSpelunkKey is null
|
||||
? $"Adding {link.Name}, the first server from Spelunk at {link.ApiBase}. Choose the credential "
|
||||
+ "holding your Spelunk API key and save — Spelunk signs you in, so no SSH key is needed, and "
|
||||
+ "later links from this Spelunk connect straight away."
|
||||
? $"Adding {link.Name}, the first server from Spelunk at {link.ApiBase}. Choose your Spelunk API "
|
||||
+ "key and save — Spelunk signs you in, so no SSH key is needed, and later links from this "
|
||||
+ "Spelunk connect straight away. Saving the key under Settings → Security does the same."
|
||||
: $"Adding {link.Name} from Spelunk at {link.ApiBase}. Check it and save.";
|
||||
|
||||
Status = said;
|
||||
@@ -484,10 +489,20 @@ internal sealed partial class VaultViewModel
|
||||
}
|
||||
|
||||
/// <summary>Fills the API key picker, keeping whatever the host names selectable.</summary>
|
||||
/// <remarks>
|
||||
/// Saved Spelunk keys first, named for where they go, because that is the choice the Security page made
|
||||
/// for exactly this; then every credential, for a key kept as a password before saved keys existed.
|
||||
/// </remarks>
|
||||
private void BuildSpelunkKeyChoices(Guid? selectedId)
|
||||
{
|
||||
EditorSpelunkKeyChoices.Clear();
|
||||
|
||||
foreach (var key in SpelunkKeys)
|
||||
{
|
||||
EditorSpelunkKeyChoices.Add(new AuthenticationChoice(
|
||||
AuthenticationKind.Credential, key.EntityId, key.Label, $"Spelunk key · {key.ApiBase}"));
|
||||
}
|
||||
|
||||
foreach (var credential in Credentials)
|
||||
{
|
||||
EditorSpelunkKeyChoices.Add(AuthenticationChoice.ForCredential(credential.EntityId, credential.Label));
|
||||
|
||||
Reference in new issue
Block a user