Update deterministic key implementation

This commit is contained in:
nicolas.dorier 2023-10-24 16:53:49 +09:00
parent b8a7bd5d73
commit b443dde26b
No known key found for this signature in database
GPG Key ID: 6618763EF09186FE
8 changed files with 130 additions and 122 deletions

View File

@ -4,7 +4,7 @@
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>1.0.12</Version>
<Version>1.0.13</Version>
</PropertyGroup>
<PropertyGroup>

View File

@ -126,58 +126,58 @@ public class AESKey
return ms.ToArray();
}
public byte[] CMac(byte[] data)
public byte[] CMac(byte[] data)
{
var key = _bytes;
// SubKey generation
// step 1, AES-128 with key K is applied to an all-zero input block.
byte[] L = AesEncrypt(key, new byte[16], new byte[16]);
// step 2, K1 is derived through the following operation:
byte[]
FirstSubkey =
RotateLeft(L); //If the most significant bit of L is equal to 0, K1 is the left-shift of L by 1 bit.
if ((L[0] & 0x80) == 0x80)
FirstSubkey[15] ^=
0x87; // Otherwise, K1 is the exclusive-OR of const_Rb and the left-shift of L by 1 bit.
// step 3, K2 is derived through the following operation:
byte[]
SecondSubkey =
RotateLeft(FirstSubkey); // If the most significant bit of K1 is equal to 0, K2 is the left-shift of K1 by 1 bit.
if ((FirstSubkey[0] & 0x80) == 0x80)
SecondSubkey[15] ^=
0x87; // Otherwise, K2 is the exclusive-OR of const_Rb and the left-shift of K1 by 1 bit.
// MAC computing
if (((data.Length != 0) && (data.Length % 16 == 0)) == true)
{
var key = _bytes;
// SubKey generation
// step 1, AES-128 with key K is applied to an all-zero input block.
byte[] L = AesEncrypt(key, new byte[16], new byte[16]);
// step 2, K1 is derived through the following operation:
byte[]
FirstSubkey =
RotateLeft(L); //If the most significant bit of L is equal to 0, K1 is the left-shift of L by 1 bit.
if ((L[0] & 0x80) == 0x80)
FirstSubkey[15] ^=
0x87; // Otherwise, K1 is the exclusive-OR of const_Rb and the left-shift of L by 1 bit.
// step 3, K2 is derived through the following operation:
byte[]
SecondSubkey =
RotateLeft(FirstSubkey); // If the most significant bit of K1 is equal to 0, K2 is the left-shift of K1 by 1 bit.
if ((FirstSubkey[0] & 0x80) == 0x80)
SecondSubkey[15] ^=
0x87; // Otherwise, K2 is the exclusive-OR of const_Rb and the left-shift of K1 by 1 bit.
// MAC computing
if (((data.Length != 0) && (data.Length % 16 == 0)) == true)
{
// If the size of the input message block is equal to a positive multiple of the block size (namely, 128 bits),
// the last block shall be exclusive-OR'ed with K1 before processing
for (int j = 0; j < FirstSubkey.Length; j++)
data[data.Length - 16 + j] ^= FirstSubkey[j];
}
else
{
// Otherwise, the last block shall be padded with 10^i
byte[] padding = new byte[16 - data.Length % 16];
padding[0] = 0x80;
data = data.Concat(padding.AsEnumerable()).ToArray();
// and exclusive-OR'ed with K2
for (int j = 0; j < SecondSubkey.Length; j++)
data[data.Length - 16 + j] ^= SecondSubkey[j];
}
// The result of the previous process will be the input of the last encryption.
byte[] encResult = AesEncrypt(key, new byte[16], data);
byte[] HashValue = new byte[16];
Array.Copy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length);
return HashValue;
// If the size of the input message block is equal to a positive multiple of the block size (namely, 128 bits),
// the last block shall be exclusive-OR'ed with K1 before processing
for (int j = 0; j < FirstSubkey.Length; j++)
data[data.Length - 16 + j] ^= FirstSubkey[j];
}
else
{
// Otherwise, the last block shall be padded with 10^i
byte[] padding = new byte[16 - data.Length % 16];
padding[0] = 0x80;
data = data.Concat(padding.AsEnumerable()).ToArray();
// and exclusive-OR'ed with K2
for (int j = 0; j < SecondSubkey.Length; j++)
data[data.Length - 16 + j] ^= SecondSubkey[j];
}
// The result of the previous process will be the input of the last encryption.
byte[] encResult = AesEncrypt(key, new byte[16], data);
byte[] HashValue = new byte[16];
Array.Copy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length);
return HashValue;
}
static byte[] RotateLeft(byte[] b)
{

View File

@ -5,7 +5,7 @@
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
<Version>1.0.12</Version>
<Version>1.0.13</Version>
</PropertyGroup>
<PropertyGroup>

View File

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.NTag424;
public record CardKey(AESKey AESKey)
{
public CardKey(byte[] bytes) : this(new AESKey(bytes))
{
}
public AESKey DeriveAuthenticationKey(byte[] uid)
{
Helpers.ValidateUID(uid);
return AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x78 },
uid));
}
public BoltcardKeys DeriveBoltcardKeys(IssuerKey issuerKey, byte[] uid)
{
return DeriveBoltcardKeys(issuerKey.DeriveEncryptionKey(), uid);
}
public BoltcardKeys DeriveBoltcardKeys(AESKey encryptionKey, byte[] uid)
{
Helpers.ValidateUID(uid);
var appMasterKey = AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x76 },
uid));
var authKey = DeriveAuthenticationKey(uid);
var k1 = AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x79 },
uid));
var k2 = AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x7a },
uid));
return new BoltcardKeys(appMasterKey, encryptionKey, authKey, k1, k2);
}
public bool CheckSunMac([NotNullWhen(true)] Uri? uri, BoltcardPICCData piccData)
{
if (!PICCData.ExtractPC(uri, out _, out var c))
return false;
return this.DeriveAuthenticationKey(piccData.Uid).CheckSunMac(c, piccData);
}
public bool CheckSunMac([NotNullWhen(true)] string? c, BoltcardPICCData piccData)
{
return this.DeriveAuthenticationKey(piccData.Uid).CheckSunMac(c, piccData);
}
}

View File

@ -70,6 +70,12 @@ internal class Helpers
}
return res;
}
public static void ValidateUID(byte[] uid)
{
ArgumentNullException.ThrowIfNull(uid);
if (uid.Length != 7)
throw new ArgumentException("uid should be 7 bytes", nameof(uid));
}
//https://github.com/alexgorbatchev/crc/blob/master/src/calculators/crcjam.ts

View File

@ -28,57 +28,15 @@ public record IssuerKey(AESKey AESKey)
return AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x77 }));
}
private void Validate(byte[] uid)
{
ArgumentNullException.ThrowIfNull(uid);
if (uid.Length != 7)
throw new ArgumentException("uid should be 7 bytes", nameof(uid));
}
private void Validate(byte[] uid, byte[] nonce)
{
Validate(uid);
ArgumentNullException.ThrowIfNull(nonce);
if (nonce.Length != 16)
throw new ArgumentException("nonce should be 16 bytes", nameof(nonce));
}
public AESKey DeriveAuthenticationKey(byte[] uid, byte[] nonce)
{
Validate(uid, nonce);
return AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x78 },
uid,
nonce));
}
public BoltcardKeys DeriveBoltcardKeys(byte[] uid, byte[] nonce)
{
Validate(uid, nonce);
var appMasterKey = AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x76 },
uid,
nonce));
var encryptionKey = DeriveEncryptionKey();
var authKey = DeriveAuthenticationKey(uid, nonce);
var k1 = AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x79 },
uid,
nonce));
var k2 = AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x7a },
uid,
nonce));
return new BoltcardKeys(appMasterKey, encryptionKey, authKey, k1, k2);
}
/// <summary>
/// Get the ID from the UID
/// </summary>
/// <param name="uid">The UID</param>
/// <returns>The ID</returns>
/// <returns>The ID used to fetch CardKey from database</returns>
public byte[] GetId(byte[] uid)
{
Validate(uid);
Helpers.ValidateUID(uid);
return AESKey.Derive(Helpers.Concat(
new byte[] { 0x2d, 0x00, 0x3f, 0x7b },
uid)).ToBytes().ToArray();
@ -107,15 +65,4 @@ public record IssuerKey(AESKey AESKey)
{
return BoltcardPICCData.TryDecrypt(DeriveEncryptionKey(), p, c, payload);
}
public bool CheckSunMac([NotNullWhen(true)] Uri? uri, BoltcardPICCData piccData, byte[] nonce)
{
if (!PICCData.ExtractPC(uri, out _, out var c))
return false;
return this.DeriveAuthenticationKey(piccData.Uid, nonce).CheckSunMac(c, piccData);
}
public bool CheckSunMac([NotNullWhen(true)] string? c, BoltcardPICCData piccData, byte[] nonce)
{
return this.DeriveAuthenticationKey(piccData.Uid, nonce).CheckSunMac(c, piccData);
}
}

View File

@ -395,9 +395,10 @@ public class Ntag424
/// Reset the card to factory settings using current application keys using deterministic keys
/// </summary>
/// <param name="issuerKey">The issuer key</param>
/// <param name="cardKey">The card key</param>
/// <param name="nonce"></param>
/// <returns></returns>
public async Task ResetCard(IssuerKey issuerKey, byte[] nonce)
public async Task ResetCard(IssuerKey issuerKey, CardKey cardKey)
{
var encryptionKey = issuerKey.DeriveEncryptionKey();
if (CurrentSession is null)
@ -405,7 +406,7 @@ public class Ntag424
if (encryptionKey != CurrentSession!.Key)
await AuthenticateEV2NonFirst(1, encryptionKey);
var uid = await GetCardUID();
var keys = issuerKey.DeriveBoltcardKeys(uid, nonce);
var keys = cardKey.DeriveBoltcardKeys(issuerKey, uid);
await ResetCard(keys);
}

View File

@ -78,9 +78,9 @@ public class UnitTest1
public void CanDeriveDeterministicBoltcard()
{
var uid = "04a39493cc8680".HexToBytes();
var nonce = "b45775776cb224c75bcde7ca3704e933".HexToBytes();
var issuerKey = new IssuerKey("00000000000000000000000000000001".HexToBytes());
var keys = issuerKey.DeriveBoltcardKeys(uid, nonce);
var cardKey = new CardKey("00000000000000000000000000000002".HexToBytes());
var keys = cardKey.DeriveBoltcardKeys(issuerKey, uid);
Logs.WriteLine("K0: " + keys.AppMasterKey.ToBytes().ToHex());
Logs.WriteLine("K1: " + keys.EncryptionKey.ToBytes().ToHex());
Logs.WriteLine("K2: " + keys.AuthenticationKey.ToBytes().ToHex());
@ -180,14 +180,13 @@ public class UnitTest1
using var ctx = await PCSCContext.WaitForCard();
var ntag = ctx.CreateNTag424();
var issuerKey = new IssuerKey("00000000000000000000000000000001".HexToBytes());
var cardKey = new CardKey("00000000000000000000000000000002".HexToBytes());
// First time authenticate is with the default 00.000 key
await ntag.AuthenticateEV2First(0, AESKey.Default);
var uid = await ntag.GetCardUID();
var nonce = "00010000000000000000000000000000".HexToBytes();
var keys = issuerKey.DeriveBoltcardKeys(uid, nonce);
var keys = cardKey.DeriveBoltcardKeys(issuerKey, uid);
await ntag.SetupBoltcard("lnurlw://blahblah.com", BoltcardKeys.Default, keys);
var uri = await ntag.TryReadNDefURI();
@ -197,9 +196,9 @@ public class UnitTest1
// In real life, you would fetch the nonce from database
// var nonce = await FetchNonce(issuerKey.GetId(piccData.Uid));
Assert.True(issuerKey.CheckSunMac(uri, piccData, nonce));
Assert.True(cardKey.CheckSunMac(uri, piccData));
await ntag.ResetCard(issuerKey, nonce);
await ntag.ResetCard(issuerKey, cardKey);
// If this method didn't throw an exception, it has been successfully decrypted and authenticated.
// You can reset the card with `await ntag.ResetCard(issuerKey, nonce);`.
}
@ -207,17 +206,17 @@ public class UnitTest1
[Fact]
public async Task Reset()
{
var issuerKey = new IssuerKey("01000000000000000000000000000000".HexToBytes());
var nonce = "00010000000000000000000000000000".HexToBytes();
var issuerKey = new IssuerKey("00000000000000000000000000000001".HexToBytes());
var cardKey = new CardKey("00000000000000000000000000000002".HexToBytes());
using var ctx = PCSCContext.Create();
var ntag = ctx.CreateNTag424();
await ntag.AuthenticateEV2First(0, AESKey.Default);
var uid = await ntag.GetCardUID();
var keys = issuerKey.DeriveBoltcardKeys(uid, nonce);
var keys = cardKey.DeriveBoltcardKeys(issuerKey, uid);
await ntag.SetupBoltcard("lnurlw://test.com", BoltcardKeys.Default, keys);
await ntag.ResetCard(issuerKey, nonce);
await ntag.ResetCard(issuerKey, cardKey);
}
[Fact]
@ -271,18 +270,20 @@ public class UnitTest1
using var ctx = PCSCContext.Create();
var ntag = ctx.CreateNTag424();
var issuerKey = new IssuerKey("00000000000000000000000000000001".HexToBytes());
var cardKey = new CardKey("00000000000000000000000000000002".HexToBytes());
var nonce = "00010000000000000000000000000000".HexToBytes();
//await ntag.ResetCard(issuerKey, nonce);
await ntag.AuthenticateEV2First(0, AESKey.Default);
var uid = await ntag.GetCardUID();
var keys = issuerKey.DeriveBoltcardKeys(uid, nonce);
var keys = cardKey.DeriveBoltcardKeys(issuerKey, uid);
await ntag.SetupBoltcard("http://test.com", BoltcardKeys.Default, keys);
var uri = await ntag.TryReadNDefURI();
issuerKey.TryDecrypt(uri);
var piccData = issuerKey.TryDecrypt(uri);
Assert.NotNull(piccData);
await ntag.ResetCard(issuerKey, nonce);
await ntag.ResetCard(issuerKey, cardKey);
}
[Fact]