Files
DodoSSH/src/DodoSSH.Client.App/Platform/DeviceKeyNames.cs
T
jaap-jan 0e8fe4fc80 Give each profile its own device key, and make the Linux PIN window able to open at all
- The Windows CNG key and the macOS keychain tag were one name for every profile, so signing out of
  the nightly deleted the key the release build's device unlock was sealed to. DeviceKeyNames keeps the
  existing name for the default profile and gives every other profile its own; sign-out deletes only
  its own key, and a profile sealed before this keeps reading the shared one.
- DevicePinWindow named two palette resources that do not exist and loaded itself with
  AvaloniaXamlLoader.Load, which never assigns the named fields, so it threw on construction and no
  PIN prompt was ever shown. A layout test now builds it against the real palette.
2026-10-02 13:16:48 +02:00

49 lines
2.1 KiB
C#

using System.Security.Cryptography;
using System.Text;
using DodoSSH.Client.Session;
namespace DodoSSH.Client.App.Platform;
/// <summary>
/// What a platform key store calls the hardware key behind one profile's device unlock.
/// </summary>
/// <remarks>
/// <para>
/// ◆ <b>One key per profile, because the key outlives the file and is shared by name.</b> The CNG key and
/// the Secure Enclave keychain item are found by name in a store that belongs to the user, not to the
/// profile directory. While every profile used the same name, the release build and the nightly — which
/// <see cref="ClientPaths.ForChannel"/> keeps apart precisely so neither can damage the other — shared one
/// key: signing out of either deleted it, and the other's device unlock silently stopped working.
/// </para>
/// <para>
/// <b>The default profile keeps the name it has always had</b>, so an existing install's sealed key still
/// opens. Every other profile gets that name with a digest of its directory on the end — a digest rather
/// than the directory's own name, because a portable copy can live in any folder and two such folders can
/// end in the same name.
/// </para>
/// </remarks>
internal static class DeviceKeyNames
{
/// <summary>The name of the key behind <paramref name="paths"/>' device unlock.</summary>
/// <param name="paths">The profile.</param>
/// <param name="legacy">The name every profile used before names were per profile.</param>
internal static string For(ClientPaths paths, string legacy)
{
ArgumentNullException.ThrowIfNull(paths);
var directory = Normalise(paths.DataDirectory);
if (string.Equals(directory, Normalise(ClientPaths.Default.DataDirectory), StringComparison.OrdinalIgnoreCase))
{
return legacy;
}
var digest = SHA256.HashData(Encoding.UTF8.GetBytes(directory.ToUpperInvariant()));
return $"{legacy}.{Convert.ToHexStringLower(digest.AsSpan(0, 8))}";
}
private static string Normalise(string directory) =>
Path.TrimEndingDirectorySeparator(Path.GetFullPath(directory));
}