From 689695ee99597ee3cfc497ba367ba8a489ae2ff6 Mon Sep 17 00:00:00 2001 From: Fan DANG Date: Mon, 15 Jan 2024 20:04:16 +0800 Subject: [PATCH] add pin protocol 1 support --- lib/fido2.dart | 2 +- lib/src/cose.dart | 12 +++---- lib/src/ctap2/pin.dart | 66 +++++++++++++++++++++++++++++++++++++++ lib/src/utils/string.dart | 23 -------------- pubspec.yaml | 3 ++ test/fido2_base_test.dart | 25 +++++++-------- test/fido2_test_pin.dart | 58 ++++++++++++++++++++++++++++++++++ 7 files changed, 145 insertions(+), 44 deletions(-) create mode 100644 lib/src/ctap2/pin.dart delete mode 100644 lib/src/utils/string.dart create mode 100644 test/fido2_test_pin.dart diff --git a/lib/fido2.dart b/lib/fido2.dart index 383f764..d8ddff7 100644 --- a/lib/fido2.dart +++ b/lib/fido2.dart @@ -1,4 +1,4 @@ library fido2; export 'src/ctap2/base.dart'; -export 'src/utils/string.dart'; +export 'src/ctap2/pin.dart'; diff --git a/lib/src/cose.dart b/lib/src/cose.dart index 868ff1f..e5d0ede 100644 --- a/lib/src/cose.dart +++ b/lib/src/cose.dart @@ -45,13 +45,13 @@ class ES256 extends CoseKey { ES256(Map coseKeyParams) : super(coseKeyParams); - static ES256 fromPublicKey(List publicKey) { + static ES256 fromPublicKey(List x, List y) { return ES256({ 1: 2, 3: ES256.algorithm, -1: 1, - -2: publicKey.sublist(1, 33), - -3: publicKey.sublist(33), + -2: x, + -3: y, }); } @@ -72,13 +72,13 @@ class EcdhEsHkdf256 extends CoseKey { EcdhEsHkdf256(Map coseKeyParams) : super(coseKeyParams); - static EcdhEsHkdf256 fromPublicKey(List publicKey) { + static EcdhEsHkdf256 fromPublicKey(List x, List y) { return EcdhEsHkdf256({ 1: 2, 3: EcdhEsHkdf256.algorithm, -1: 1, - -2: publicKey.sublist(0, 32), - -3: publicKey.sublist(32), + -2: x, + -3: y, }); } diff --git a/lib/src/ctap2/pin.dart b/lib/src/ctap2/pin.dart new file mode 100644 index 0000000..b03f307 --- /dev/null +++ b/lib/src/ctap2/pin.dart @@ -0,0 +1,66 @@ +import 'package:convert/convert.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:elliptic/ecdh.dart'; +import 'package:elliptic/elliptic.dart'; +import 'package:fido2/fido2.dart'; +import 'package:fido2/src/cose.dart'; +import 'package:quiver/collection.dart'; + +class EncapsulateResult { + final CoseKey coseKey; + final List sharedSecret; + + EncapsulateResult(this.coseKey, this.sharedSecret); +} + +abstract class PinProtocol { + Future encapsulate(CoseKey peerCoseKey); + + Future> encrypt(List key, List plaintext); + + Future> decrypt(List key, List ciphertext); + + Future> authenticate(List key, List message); + + Future verify(List key, List message, List signature); +} + +class PinProtocolV1 extends PinProtocol { + @override + Future encapsulate(CoseKey peerCoseKey) async { + final ec = getP256(); + final priv = ec.generatePrivateKey(); + final pub = priv.publicKey; + final pubBytes = hex.decode(pub.toHex().substring(2)); + final keyAgreement = EcdhEsHkdf256.fromPublicKey(pubBytes.sublist(0, 32), pubBytes.sublist(32, 64)); + final sharedSecret = computeSecret(priv, ec.hexToPublicKey('04${hex.encode(peerCoseKey[-2] + peerCoseKey[-3])}')); + return EncapsulateResult(keyAgreement, await sharedSecret); + } + + @override + Future> encrypt(List key, List plaintext) async { + final algorithm = AesCbc.with128bits(macAlgorithm: MacAlgorithm.empty, paddingAlgorithm: PaddingAlgorithm.zero); + final secretBox = await algorithm.encrypt(plaintext, secretKey: SecretKeyData(key), nonce: List.filled(16, 0)); + return secretBox.cipherText; + } + + @override + Future> decrypt(List key, List ciphertext) async { + final algorithm = AesCbc.with128bits(macAlgorithm: MacAlgorithm.empty, paddingAlgorithm: PaddingAlgorithm.zero); + return await algorithm.decrypt(SecretBox(ciphertext, nonce: List.filled(16, 0), mac: Mac.empty), secretKey: SecretKeyData(key)); + } + + @override + Future> authenticate(List key, List message) async { + final algorithm = Hmac.sha256(); + final mac = await algorithm.calculateMac(message, secretKey: SecretKeyData(key)); + return mac.bytes; + } + + @override + Future verify(List key, List message, List signature) async { + final algorithm = Hmac.sha256(); + final mac = await algorithm.calculateMac(message, secretKey: SecretKeyData(key)); + return listsEqual(mac.bytes, signature); + } +} diff --git a/lib/src/utils/string.dart b/lib/src/utils/string.dart deleted file mode 100644 index c1181f3..0000000 --- a/lib/src/utils/string.dart +++ /dev/null @@ -1,23 +0,0 @@ -List hexStringToList(String hex) { - List result = []; - - // Remove leading '0x' if present - if (hex.startsWith('0x')) { - hex = hex.substring(2); - } - - // Pad the string with a leading zero if it's not even in length - if (hex.length % 2 != 0) { - hex = '0$hex'; - } - - // Iterate over the string in steps of 2 characters - for (int i = 0; i < hex.length; i += 2) { - // Get a substring of two characters and convert it to an integer - String byteString = hex.substring(i, i + 2); - int byteInt = int.parse(byteString, radix: 16); - result.add(byteInt); - } - - return result; -} diff --git a/pubspec.yaml b/pubspec.yaml index 3c56659..475419c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,9 @@ environment: dependencies: cbor: ^6.1.1 convert: ^3.0.1 + cryptography: ^2.7.0 + elliptic: ^0.3.10 + quiver: ^3.2.1 dev_dependencies: lints: ^2.1.0 diff --git a/test/fido2_base_test.dart b/test/fido2_base_test.dart index e5a44ac..7f65335 100644 --- a/test/fido2_base_test.dart +++ b/test/fido2_base_test.dart @@ -1,6 +1,6 @@ +import 'package:convert/convert.dart'; import 'package:fido2/fido2.dart'; -import 'package:fido2/src/cose.dart' as cose; - +import 'package:fido2/src/cose.dart'; import 'package:test/test.dart'; void main() { @@ -11,7 +11,7 @@ void main() { }); test('Response', () { - var response = hexStringToList( + var response = hex.decode( 'AD0183665532465F5632684649444F5F325F30684649444F5F325F3102846863726564426C6F626B6372656450726F746563746B686D61632D7365637265746C6C61726765426C6F624B65790350244EB29EE0904E4981FE1F20F8D3B8F404A662726BF568637265644D676D74F569636C69656E7450696EF46A6C61726765426C6F6273F56E70696E557641757468546F6B656EF5706D616B654372656455764E6F74527164F5051905140682010207080818460982636E6663637573620A83A263616C672664747970656A7075626C69632D6B6579A263616C672764747970656A7075626C69632D6B6579A263616C67382F64747970656A7075626C69632D6B65790B1910000E18C90F1820'); var info = Ctap2.parseGetInfoResponse(response); expect(info.versions, equals(['U2F_V2', 'FIDO_2_0', 'FIDO_2_1'])); @@ -21,31 +21,28 @@ void main() { group('ClientPin', () { test('Request1', () { - var request = Ctap2.makeClientPinRequest(ClientPinRequest( - subCommand: ClientPinSubCommand.getKeyAgreement.value, - pinUvAuthProtocol: 2)); - expect(request, equals(hexStringToList('06A201020202'))); + var request = Ctap2.makeClientPinRequest(ClientPinRequest(subCommand: ClientPinSubCommand.getKeyAgreement.value, pinUvAuthProtocol: 2)); + expect(request, equals(hex.decode('06A201020202'))); }); test('Request2', () { var request = Ctap2.makeClientPinRequest(ClientPinRequest( subCommand: ClientPinSubCommand.setPin.value, pinUvAuthProtocol: 2, - keyAgreement: cose.EcdhEsHkdf256.fromPublicKey(hexStringToList( - '9950CCD8C524DBAAB6D5ED7E4256B72A647920445DCA51DA5F1B2A6AEB9AAB1880CC342ABC60C6FD1E8101CB3AA1D34B43CAFA6C3CA5403D70DEC1C72EC637FD')), - pinUvAuthParam: hexStringToList( - '9941B629D9BAB9C8C578D5E7A3AE6201B7A2F90F02B238AA2674F4A976C17FF3'), - newPinEnc: hexStringToList( + keyAgreement: EcdhEsHkdf256.fromPublicKey(hex.decode('9950CCD8C524DBAAB6D5ED7E4256B72A647920445DCA51DA5F1B2A6AEB9AAB18'), + hex.decode('80CC342ABC60C6FD1E8101CB3AA1D34B43CAFA6C3CA5403D70DEC1C72EC637FD')), + pinUvAuthParam: hex.decode('9941B629D9BAB9C8C578D5E7A3AE6201B7A2F90F02B238AA2674F4A976C17FF3'), + newPinEnc: hex.decode( '75E69079A080945600397CC32ABE3B5CFD61C1BBBAD4CE71396EBB64D51D0198CC9D6FF8EBD14A6C9A134BE717CEBFB1CB25815B3AD0080DCC7414D8604DF1729E89EA54B1277DC701077C6ED5B8512A'), )); expect( request, - equals(hexStringToList( + equals(hex.decode( '06A50102020303A5010203381820012158209950CCD8C524DBAAB6D5ED7E4256B72A647920445DCA51DA5F1B2A6AEB9AAB1822582080CC342ABC60C6FD1E8101CB3AA1D34B43CAFA6C3CA5403D70DEC1C72EC637FD0458209941B629D9BAB9C8C578D5E7A3AE6201B7A2F90F02B238AA2674F4A976C17FF305585075E69079A080945600397CC32ABE3B5CFD61C1BBBAD4CE71396EBB64D51D0198CC9D6FF8EBD14A6C9A134BE717CEBFB1CB25815B3AD0080DCC7414D8604DF1729E89EA54B1277DC701077C6ED5B8512A'))); }); test('Response1', () { - var response = hexStringToList( + var response = hex.decode( 'A101A50102033818200121582064E75C1E36EF6C3C17F609014D96D048BEB6793CD34823358E44A599B4DD2291225820235BD52FAEB2A3599F10D38EFB58E65BE58AE67AF118BF1BC528FA4B090EE763'); var clientPinResponse = Ctap2.parseClientPinResponse(response); expect(clientPinResponse.keyAgreement![1], equals(2)); diff --git a/test/fido2_test_pin.dart b/test/fido2_test_pin.dart new file mode 100644 index 0000000..f7206a5 --- /dev/null +++ b/test/fido2_test_pin.dart @@ -0,0 +1,58 @@ +import 'package:convert/convert.dart'; +import 'package:elliptic/ecdh.dart'; +import 'package:elliptic/elliptic.dart'; +import 'package:fido2/fido2.dart'; +import 'package:fido2/src/cose.dart'; +import 'package:fido2/src/ctap2/pin.dart'; +import 'package:test/test.dart'; + +void main() { + group('Protocol 1', () { + test('encapsulate', () async { + final ec = getP256(); + final priv = ec.generatePrivateKey(); + final pub = priv.publicKey; + final pubBytes = hex.decode(pub.toHex().substring(2)); + final peerCoseKey = EcdhEsHkdf256.fromPublicKey(pubBytes.sublist(0, 32), pubBytes.sublist(32, 64)); + + PinProtocolV1 pinProtocol = PinProtocolV1(); + EncapsulateResult result = await pinProtocol.encapsulate(peerCoseKey); + final sharedSecret = computeSecret(priv, ec.hexToPublicKey('04${hex.encode(result.coseKey[-2] + result.coseKey[-3])}')); + expect(sharedSecret, equals(result.sharedSecret)); + }); + + test('encrypt', () async { + final key = hex.decode('000102030405060708090a0b0c0d0e0f'); + final plaintext = hex.decode('00112233445566778899aabbccddeeff'); + final ciphertext = hex.decode('69c4e0d86a7b0430d8cdb78070b4c55a'); + PinProtocolV1 pinProtocol = PinProtocolV1(); + expect(await pinProtocol.encrypt(key, plaintext), equals(ciphertext)); + }); + + test('decrypt', () async { + final key = hex.decode('000102030405060708090a0b0c0d0e0f'); + final plaintext = hex.decode('00112233445566778899aabbccddeeff'); + final ciphertext = hex.decode('69c4e0d86a7b0430d8cdb78070b4c55a'); + PinProtocolV1 pinProtocol = PinProtocolV1(); + expect(await pinProtocol.decrypt(key, ciphertext), equals(plaintext)); + }); + + test('authenticate', () async { + final key = hex.decode('000102030405060708090a0b0c0d0e0f'); + final message = hex.decode('00112233445566778899aabbccddeeff'); + final signature = hex.decode('32cd28477b88c12e515b0e1fd7330d19616a4a51f6c502d64fe6a93fe7f786fa'); + PinProtocolV1 pinProtocol = PinProtocolV1(); + expect(await pinProtocol.authenticate(key, message), equals(signature)); + }); + + test('verify', () async { + final key = hex.decode('000102030405060708090a0b0c0d0e0f'); + final message = hex.decode('00112233445566778899aabbccddeeff'); + final signature = hex.decode('32cd28477b88c12e515b0e1fd7330d19616a4a51f6c502d64fe6a93fe7f786fa'); + final signatureFalse = hex.decode('32cd28477b88c12e515b0e1fd7330d19616a4a51f6c502d64fe6a93fe7f786fb'); + PinProtocolV1 pinProtocol = PinProtocolV1(); + expect(await pinProtocol.verify(key, message, signature), equals(true)); + expect(await pinProtocol.verify(key, message, signatureFalse), equals(false)); + }); + }); +}