Public Access
Sign in to Spelunk servers through Spelunk, and connect straight away from a Spelunk link
A Spelunk route can now say that Spelunk signs the user in. Each connection makes a throwaway Ed25519 key pair in memory, sends the public half with the ticket request ({"signIn":{"publicKey":...}}), and logs in as the account Spelunk names (root when it names none) with that key, over the agent's one-connection sshd. Such a host needs no username, key, credential or typed password: the editor greys the login out and saves none, the password box and sheet stay down, and shells, file transfer and port forwards all take the key and account from the route. The flag is nested in the spelunk object at host schema version 10; hosts without it keep their version-9 bytes.
A dodossh://spelunk/connect link now connects: to the personal vault's host tagged spelunk for that server, or, when a saved host already sends an API key to that Spelunk, to a new sign-in host saved in the personal vault with that key and the spelunk tag (made if missing). Only the first link from a Spelunk opens the editor, pre-filled with sign-in ticked and the tag on, so the key credential is chosen once and never sent to an address no saved host already sends it to.
This commit is contained in:
1 parent
07bd5c9886
commit
38ab9b431b
18 files changed
+1239
-115
No files matched your search
@@ -135,12 +135,28 @@ public static class HostSecretCodec
|
||||
/// </remarks>
|
||||
public const int SpelunkSchemaVersion = 9;
|
||||
|
||||
/// <summary>The version that introduced <see cref="SpelunkRoute.SignInThroughSpelunk"/>.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Its own version, and nested in the existing <c>spelunk</c> object rather than beside it, because it
|
||||
/// is part of that one decision. A build that knows 9 skips the unknown member and reads a valid host —
|
||||
/// a Spelunk host with no login of its own — and it must not save that back: the flag would be dropped
|
||||
/// and the host would start asking for a password it never had. Written at this version, that build
|
||||
/// shows the host, refuses to edit it, and says to update.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Only a route that signs in is written here. Every other Spelunk host stays at
|
||||
/// <see cref="SpelunkSchemaVersion"/>, byte for byte what it was.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public const int SpelunkSignInSchemaVersion = 10;
|
||||
|
||||
/// <summary>The highest schema version this build can write.</summary>
|
||||
/// <remarks>
|
||||
/// Names the highest constant above, which <see cref="SchemaVersionFor"/> assumes when it takes a
|
||||
/// maximum. A new field added below this line has to be named here too.
|
||||
/// </remarks>
|
||||
public const int CurrentSchemaVersion = SpelunkSchemaVersion;
|
||||
public const int CurrentSchemaVersion = SpelunkSignInSchemaVersion;
|
||||
|
||||
/// <summary>Serialises a host to the bytes that get sealed.</summary>
|
||||
/// <exception cref="ArgumentException">The host is not valid for storage.</exception>
|
||||
@@ -193,14 +209,7 @@ public static class HostSecretCodec
|
||||
: [.. host.PortForwards.Select(PortForwardDocument.From)],
|
||||
// Last of all, and absent for a host that is dialled, so every host without a route encodes
|
||||
// exactly as it did before routes existed.
|
||||
Spelunk = host.Spelunk is { } route
|
||||
? new SpelunkPayloadDocument
|
||||
{
|
||||
Api = route.ApiBase,
|
||||
ServerId = route.ServerId,
|
||||
ApiKeyCredentialId = route.ApiKeyCredentialId,
|
||||
}
|
||||
: null,
|
||||
Spelunk = host.Spelunk is { } route ? SpelunkPayloadDocument.From(route) : null,
|
||||
};
|
||||
|
||||
return JsonSerializer.SerializeToUtf8Bytes(
|
||||
@@ -286,6 +295,11 @@ public static class HostSecretCodec
|
||||
version = Math.Max(version, SpelunkSchemaVersion);
|
||||
}
|
||||
|
||||
if (host.SignsInThroughSpelunk)
|
||||
{
|
||||
version = Math.Max(version, SpelunkSignInSchemaVersion);
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
@@ -437,6 +451,9 @@ public static class HostSecretCodec
|
||||
ApiBase = parsed.Api ?? string.Empty,
|
||||
ServerId = parsed.ServerId ?? Guid.Empty,
|
||||
ApiKeyCredentialId = parsed.ApiKeyCredentialId ?? Guid.Empty,
|
||||
|
||||
// False folds to the default, as AsksForPassword's does: one state, one spelling.
|
||||
SignInThroughSpelunk = parsed.SignIn is true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -528,6 +545,23 @@ internal sealed class SpelunkPayloadDocument
|
||||
public Guid? ServerId { get; set; }
|
||||
|
||||
public Guid? ApiKeyCredentialId { get; set; }
|
||||
|
||||
/// <remarks>
|
||||
/// Last, and null unless true, so a route that does not sign in keeps the bytes it had at version 9.
|
||||
/// See <see cref="HostSecretCodec.SpelunkSignInSchemaVersion"/>.
|
||||
/// </remarks>
|
||||
public bool? SignIn { get; set; }
|
||||
|
||||
public static SpelunkPayloadDocument From(SpelunkRoute route) => new()
|
||||
{
|
||||
Api = route.ApiBase,
|
||||
ServerId = route.ServerId,
|
||||
ApiKeyCredentialId = route.ApiKeyCredentialId,
|
||||
|
||||
// Null rather than false, so a route that does not sign in encodes exactly as it did before the
|
||||
// field existed — the rule AsksForPassword follows.
|
||||
SignIn = route.SignInThroughSpelunk ? true : null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>One forward's serialised shape. Nullable throughout, because it models untrusted input.</summary>
|
||||
|
||||
Reference in new issue
Block a user