Update deterministic key implementation
This commit is contained in:
parent
b8a7bd5d73
commit
b443dde26b
@ -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>
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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>
|
||||
|
||||
53
src/BTCPayServer.NTag424/CardKey.cs
Normal file
53
src/BTCPayServer.NTag424/CardKey.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user