Deterministic boltcards
This commit is contained in:
parent
65d575849c
commit
cff973b18a
32
README.md
32
README.md
@ -122,6 +122,38 @@ await ntag.SetupBoltcard(lnurlwService, BoltcardKeys.Default, keys);
|
||||
// You can reset the card to its factory state with `await ntag.ResetCard(keys);`
|
||||
```
|
||||
|
||||
### How to setup a bolt card with deterministic keys, and decrypt the PICCData
|
||||
|
||||
Deterministic keys are useful if you want to be able to recover the keys of the card from a seed.
|
||||
* The issuer can recover the keys of any card, just with a batchId and the issuer key.
|
||||
* The LNUrlw service can recover the keys of any card (except the issuer key), just with the encryption key.
|
||||
|
||||
Note that you can reset the card to its factory state by only knowing the `issuerKey` with `await ntag.ResetCard(issuerKey);`.
|
||||
|
||||
```csharp
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.NTag424.PCSC;
|
||||
using System;
|
||||
using System.Collections;
|
||||
|
||||
using var ctx = PCSCContext.Create();
|
||||
var ntag = ctx.CreateNTag424();
|
||||
|
||||
await ntag.AuthenticateEV2First(0, AESKey.Default);
|
||||
var uid = await ntag.GetCardUID();
|
||||
|
||||
var issuerKey = new AESKey("00000000000000000000000000000001".HexToBytes());
|
||||
var keys = BoltcardKeys.CreateDeterministicKeys(issuerKey, uid, batchId: 0);
|
||||
var lnurlwService = "lnurlw://test.com";
|
||||
|
||||
var encryptionKey = keys.EncryptionKey;
|
||||
var piccData = PICCData.BoltcardDecrypt(encryptionKey, p, c);
|
||||
|
||||
// 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);`.
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using static BTCPayServer.NTag424.Helpers;
|
||||
|
||||
@ -79,10 +80,6 @@ public class AESKey
|
||||
var cmac = derived.CMac(payload);
|
||||
return Truncate(cmac);
|
||||
}
|
||||
public PICCData DecryptSun(byte[] data)
|
||||
{
|
||||
return PICCData.Create(Decrypt(data));
|
||||
}
|
||||
AESKey SesSDMFileReadMACKey(byte[]? uid, int? counter)
|
||||
{
|
||||
int i = 0;
|
||||
@ -221,4 +218,52 @@ public class AESKey
|
||||
code.AddBytes(_bytes);
|
||||
return code.ToHashCode();
|
||||
}
|
||||
|
||||
public static AESKey Random()
|
||||
{
|
||||
return new AESKey(RandomNumberGenerator.GetBytes(BLOCK_SIZE));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new boltcard encryption key from the issuer key and a batch id
|
||||
/// CMacDerive(encryptionKey, "2d003f77" + batchId) where batchId is LE encoded on 4 bytes.
|
||||
/// </summary>
|
||||
/// <param name="batch">Batch id</param>
|
||||
/// <returns>The encryption key for the batch of boltcard</returns>
|
||||
public AESKey DeriveEncryptionKey(uint batchId = 0)
|
||||
{
|
||||
return Derive(Helpers.Concat(
|
||||
new byte[] { 0x2d, 0x00, 0x3f, 0x77 },
|
||||
UIntToBytesLE(batchId)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new boltcard authentication key from the encryption key and uid.
|
||||
/// CMacDerive(encryptionKey, "2d003f78" + uid)
|
||||
/// </summary>
|
||||
/// <param name="uid">UID of the card</param>
|
||||
/// <returns>Authentication key of the card</returns>
|
||||
public AESKey DeriveAuthenticationKey(byte[] uid)
|
||||
{
|
||||
return Derive(Helpers.Concat(
|
||||
new byte[] { 0x2d, 0x00, 0x3f, 0x78 },
|
||||
uid));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new boltcard k3 and k4 key from the encryption key and uid.
|
||||
/// Those keys aren't used by the remark of ntag 424 cards indicates it should be set on the card.
|
||||
/// CMacDerive(encryptionKey, "2d003f79" + uid), CMacDerive(encryptionKey, "2d003f7a" + uid)
|
||||
/// </summary>
|
||||
/// <param name="uid">UID of the card</param>
|
||||
/// <returns>The 0x03 and 0x04 keys</returns>
|
||||
public (AESKey K1, AESKey K2) DeriveK3K4(byte[] uid)
|
||||
{
|
||||
return (Derive(Helpers.Concat(
|
||||
new byte[] { 0x2d, 0x00, 0x3f, 0x79 },
|
||||
uid)),
|
||||
Derive(Helpers.Concat(
|
||||
new byte[] { 0x2d, 0x00, 0x3f, 0x7a },
|
||||
uid)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace BTCPayServer.NTag424;
|
||||
@ -21,4 +22,12 @@ public record BoltcardKeys(
|
||||
public BoltcardKeys() : this (AESKey.Default, AESKey.Default, AESKey.Default, AESKey.Default, AESKey.Default)
|
||||
{
|
||||
}
|
||||
|
||||
public static BoltcardKeys CreateDeterministicKeys(AESKey issuerKey, byte[] uid, uint batchId = 0)
|
||||
{
|
||||
var encryptionKey = issuerKey.DeriveEncryptionKey(batchId);
|
||||
var authenticationKey = encryptionKey.DeriveAuthenticationKey(uid);
|
||||
(var k3, var k4) = encryptionKey.DeriveK3K4(uid);
|
||||
return new BoltcardKeys(issuerKey, encryptionKey, authenticationKey, k3, k4);
|
||||
}
|
||||
}
|
||||
|
||||
@ -375,6 +375,16 @@ public class Ntag424
|
||||
CurrentSession = null;
|
||||
}
|
||||
|
||||
public async Task ResetCard(AESKey issuerKey, uint batchId = 0)
|
||||
{
|
||||
if (CurrentSession is null)
|
||||
await AuthenticateEV2First(0, issuerKey);
|
||||
if (issuerKey != CurrentSession!.Key)
|
||||
await AuthenticateEV2NonFirst(0, issuerKey);
|
||||
var uid = await GetCardUID();
|
||||
var keys = BoltcardKeys.CreateDeterministicKeys(issuerKey, uid, batchId);
|
||||
await ResetCard(keys);
|
||||
}
|
||||
public async Task ResetCard(BoltcardKeys keys)
|
||||
{
|
||||
if (CurrentSession is null)
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
|
||||
using System;
|
||||
using System.Runtime.Loader;
|
||||
using System.Security;
|
||||
|
||||
namespace BTCPayServer.NTag424;
|
||||
|
||||
@ -25,4 +27,35 @@ public record PICCData(byte[]? Uid, int? Counter)
|
||||
}
|
||||
return new PICCData(uid, counter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypt the PICCData from the BoltCard and check the checksum.
|
||||
/// It assumes the authentication key has been derived from the encryption key. (ie. CMacDerive(encryptionKey, '2d003f78' + uid))
|
||||
/// </summary>
|
||||
/// <param name="encryptionKey">The encryption key for p</param>
|
||||
/// <param name="p">The p= parameter from the lnurlw (encrypted PICCData)</param>
|
||||
/// <param name="c">THe c= parameter from the lnurlw (checksum)</param>
|
||||
/// <param name="payload">Optional payload committed by c</param>
|
||||
/// <returns>The PICCData if the checksum passed verification.</returns>
|
||||
/// <exception cref="SecurityException">Invalid PICCData or checksum</exception>
|
||||
public static PICCData BoltcardDecrypt(AESKey encryptionKey, string p, string c, byte[]? payload = null)
|
||||
{
|
||||
var bytes = encryptionKey.Decrypt(p.HexToBytes());
|
||||
PICCData piccData;
|
||||
try
|
||||
{
|
||||
piccData = PICCData.Create(bytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new SecurityException("Invalid PICCData");
|
||||
}
|
||||
if (piccData.Uid is null)
|
||||
throw new SecurityException("No UID found in the PICCData");
|
||||
|
||||
var authenticationKey = encryptionKey.DeriveAuthenticationKey(piccData.Uid);
|
||||
if (!authenticationKey.CheckSunMac(c, piccData, payload))
|
||||
throw new SecurityException("Incorrect checksum for the PICCDAta");
|
||||
return piccData;
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ public class UnitTest1
|
||||
public void CanDecryptSunPICCData(string encrypted, string uid, int ctr)
|
||||
{
|
||||
var key = new AESKey(Convert.FromHexString("0c3b25d92b38ae443229dd59ad34b85d"));
|
||||
var picc = key.DecryptSun(encrypted.HexToBytes());
|
||||
var picc = PICCData.Create(key.Decrypt(encrypted.HexToBytes()));
|
||||
Assert.Equal(ctr, picc.Counter);
|
||||
Assert.Equal(uid.ToLowerInvariant(), picc.Uid?.ToHex());
|
||||
}
|
||||
@ -172,7 +172,6 @@ public class UnitTest1
|
||||
{
|
||||
using var ctx = PCSCContext.Create();
|
||||
var ntag = ctx.CreateNTag424();
|
||||
var initialKey = AESKey.Default;
|
||||
var keys = new BoltcardKeys(
|
||||
IssuerKey: new AESKey("00000000000000000000000000000001".HexToBytes()),
|
||||
EncryptionKey: new AESKey("00000000000000000000000000000002".HexToBytes()),
|
||||
@ -190,6 +189,29 @@ public class UnitTest1
|
||||
await ntag.ResetCard(keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanDoDeterministicBoltcard()
|
||||
{
|
||||
using var ctx = PCSCContext.Create();
|
||||
var ntag = ctx.CreateNTag424();
|
||||
var issuerKey = new AESKey("00000000000000000000000000000001".HexToBytes());
|
||||
// await ntag.ResetCard(issuerKey);
|
||||
await ntag.AuthenticateEV2First(0, AESKey.Default);
|
||||
var uid = await ntag.GetCardUID();
|
||||
var keys = BoltcardKeys.CreateDeterministicKeys(issuerKey, uid, batchId: 0);
|
||||
|
||||
// await ntag.ResetCard(keys);
|
||||
await ntag.SetupBoltcard("http://test.com", BoltcardKeys.Default, keys);
|
||||
var message = await ntag.ReadNDef();
|
||||
var uri = new NdefUriRecord(message[0]).Uri;
|
||||
var p = Regex.Match(uri, "p=(.*?)&").Groups[1].Value;
|
||||
var c = Regex.Match(uri, "c=(.*)").Groups[1].Value;
|
||||
|
||||
var encryptionKey = keys.EncryptionKey;
|
||||
var piccData = PICCData.BoltcardDecrypt(encryptionKey, p, c);
|
||||
await ntag.ResetCard(issuerKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCalculateCRC()
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user