Public Access
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.
This commit is contained in:
1 parent
2ff9dff560
commit
0e8fe4fc80
7 files changed
+213
-22
No files matched your search
@@ -78,11 +78,21 @@ public sealed partial class MacDeviceKeyStore : IDeviceKeyStore
|
||||
|
||||
private readonly ClientPaths paths;
|
||||
|
||||
/// <summary>This profile's own enclave key; see <see cref="DeviceKeyNames"/>.</summary>
|
||||
/// <remarks>
|
||||
/// <see cref="KeyTag"/> for the default profile and a tag of its own for every other, so that signing
|
||||
/// out of one installation cannot delete the key another's Touch ID unlock is sealed to. A profile
|
||||
/// registered before this is still sealed to the shared tag and is read from there until it registers
|
||||
/// again; it is never deleted from a profile that does not own it.
|
||||
/// </remarks>
|
||||
private readonly string keyTag;
|
||||
|
||||
/// <summary>Creates the store.</summary>
|
||||
public MacDeviceKeyStore(ClientPaths paths)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(paths);
|
||||
this.paths = paths;
|
||||
keyTag = DeviceKeyNames.For(paths, KeyTag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -163,7 +173,7 @@ public sealed partial class MacDeviceKeyStore : IDeviceKeyStore
|
||||
ReadOnlyMemory<byte> devicePrivateKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sealedKey = Seal(devicePrivateKey.Span)
|
||||
var sealedKey = Seal(devicePrivateKey.Span, keyTag)
|
||||
?? throw new InvalidOperationException(
|
||||
"The Secure Enclave would not seal the device key. Check IsAvailableAsync before offering to register one.");
|
||||
|
||||
@@ -184,7 +194,8 @@ public sealed partial class MacDeviceKeyStore : IDeviceKeyStore
|
||||
var sealedKey = await File.ReadAllBytesAsync(paths.DeviceKeyFile, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Unseal(sealedKey);
|
||||
// The shared tag only while this profile has no key of its own, so one unlock is one prompt.
|
||||
return Unseal(sealedKey, !string.Equals(keyTag, KeyTag, StringComparison.Ordinal) && !HasKey(keyTag) ? KeyTag : keyTag);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -202,7 +213,7 @@ public sealed partial class MacDeviceKeyStore : IDeviceKeyStore
|
||||
// Discarded, and that is deliberate: there is nothing a caller could do about a failure here,
|
||||
// and the file deleted above is the half that decides whether unlock will try at all. A key
|
||||
// left in the enclave with no ciphertext to open is inert.
|
||||
_ = DeleteKey(symbols, KeyTag);
|
||||
_ = DeleteKey(symbols, keyTag);
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
@@ -212,7 +223,26 @@ public sealed partial class MacDeviceKeyStore : IDeviceKeyStore
|
||||
/// Silent: it uses only the public half. Null on every failure, and the caller's answer to all of
|
||||
/// them is the same — do not offer a device unlock.
|
||||
/// </remarks>
|
||||
private static byte[]? Seal(ReadOnlySpan<byte> devicePrivateKey)
|
||||
/// <summary>Whether a key is filed under this tag, asked without using it and so without a prompt.</summary>
|
||||
private static bool HasKey(string tag)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = new CoreFoundationScope();
|
||||
|
||||
var symbols = MacSymbols.Resolve();
|
||||
|
||||
return symbols.Complete && FindKey(scope, symbols, tag, prompt: null) != IntPtr.Zero;
|
||||
}
|
||||
catch (Exception exception) when (exception is DllNotFoundException
|
||||
or EntryPointNotFoundException
|
||||
or BadImageFormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[]? Seal(ReadOnlySpan<byte> devicePrivateKey, string tag)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -227,11 +257,11 @@ public sealed partial class MacDeviceKeyStore : IDeviceKeyStore
|
||||
|
||||
// Created on first use rather than at registration, so that a device key re-registered after
|
||||
// a ForgetAsync gets a key again without anything having to notice that it had gone.
|
||||
var privateKey = FindKey(scope, symbols, KeyTag, prompt: null);
|
||||
var privateKey = FindKey(scope, symbols, tag, prompt: null);
|
||||
|
||||
if (privateKey == IntPtr.Zero)
|
||||
{
|
||||
privateKey = CreateEnclaveKey(scope, symbols, KeyTag);
|
||||
privateKey = CreateEnclaveKey(scope, symbols, tag);
|
||||
}
|
||||
|
||||
if (privateKey == IntPtr.Zero)
|
||||
@@ -281,7 +311,7 @@ public sealed partial class MacDeviceKeyStore : IDeviceKeyStore
|
||||
/// on a thread that is also expected to draw the window behind it.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private static byte[]? Unseal(byte[] sealedKey)
|
||||
private static byte[]? Unseal(byte[] sealedKey, string tag)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -294,7 +324,7 @@ public sealed partial class MacDeviceKeyStore : IDeviceKeyStore
|
||||
return null;
|
||||
}
|
||||
|
||||
var privateKey = FindKey(scope, symbols, KeyTag, ConsentPrompt);
|
||||
var privateKey = FindKey(scope, symbols, tag, ConsentPrompt);
|
||||
|
||||
if (privateKey == IntPtr.Zero)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user