From 113f6920a9c9aba8f7584883656fb2b9f536148a Mon Sep 17 00:00:00 2001 From: Tariq Zaid Date: Sat, 11 Oct 2025 18:02:10 +0200 Subject: [PATCH 1/2] X25519, Ed25519 ffi and js wrappers. --- README.md | 2 + example/pubspec.lock | 2 +- .../boringssl/lookup/symbols.generated.dart | 24 + lib/src/impl_ffi/impl_ffi.crv25519.dart | 132 ++++++ lib/src/impl_ffi/impl_ffi.dart | 15 + lib/src/impl_ffi/impl_ffi.ed25519.dart | 213 +++++++++ lib/src/impl_ffi/impl_ffi.utils.dart | 44 ++ lib/src/impl_ffi/impl_ffi.x25519.dart | 196 ++++++++ lib/src/impl_interface/impl_interface.dart | 6 + .../impl_interface.ed25519.dart | 40 ++ .../impl_interface/impl_interface.x25519.dart | 39 ++ lib/src/impl_js/impl_js.dart | 14 + lib/src/impl_js/impl_js.ed25519.dart | 157 +++++++ lib/src/impl_js/impl_js.x25519.dart | 171 +++++++ lib/src/impl_stub/impl_stub.dart | 14 + lib/src/impl_stub/impl_stub.ed25519.dart | 58 +++ lib/src/impl_stub/impl_stub.x25519.dart | 47 ++ lib/src/testing/testing.dart | 26 +- lib/src/testing/webcrypto/ed25519.dart | 83 ++++ lib/src/testing/webcrypto/x25519.dart | 93 ++++ lib/src/third_party/boringssl/ffigen.yaml | 14 + .../boringssl/generated_bindings.dart | 311 +++++++++++++ lib/src/webcrypto/webcrypto.dart | 2 + lib/src/webcrypto/webcrypto.ed25519.dart | 437 ++++++++++++++++++ lib/src/webcrypto/webcrypto.x25519.dart | 380 +++++++++++++++ src/symbols.generated.c | 12 + src/symbols.yaml | 12 + 27 files changed, 2532 insertions(+), 12 deletions(-) create mode 100644 lib/src/impl_ffi/impl_ffi.crv25519.dart create mode 100644 lib/src/impl_ffi/impl_ffi.ed25519.dart create mode 100644 lib/src/impl_ffi/impl_ffi.x25519.dart create mode 100644 lib/src/impl_interface/impl_interface.ed25519.dart create mode 100644 lib/src/impl_interface/impl_interface.x25519.dart create mode 100644 lib/src/impl_js/impl_js.ed25519.dart create mode 100644 lib/src/impl_js/impl_js.x25519.dart create mode 100644 lib/src/impl_stub/impl_stub.ed25519.dart create mode 100644 lib/src/impl_stub/impl_stub.x25519.dart create mode 100644 lib/src/testing/webcrypto/ed25519.dart create mode 100644 lib/src/testing/webcrypto/x25519.dart create mode 100644 lib/src/webcrypto/webcrypto.ed25519.dart create mode 100644 lib/src/webcrypto/webcrypto.x25519.dart diff --git a/README.md b/README.md index 43fc5765..e9c2e093 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,11 @@ Future main() async { * RSASSA-PKCS1-v1_5 (sign/verify) * RSA-PSS (sign/verify) * ECDSA (sign/verify) + * EdDSA (Ed25519) (sign/verify) * RSA-OAEP (encrypt/decrypt) * AES-CTR, AES-CBC, AES-GCM (encrypt/decrypt) * ECDH (deriveBits) + * X25519 (deriveBits) * HKDF (deriveBits) * PBKDF2 (deriveBits) * BoringSSL, Chrome and Firefox implementations pass the same test cases. diff --git a/example/pubspec.lock b/example/pubspec.lock index e619be81..8d924925 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -519,7 +519,7 @@ packages: path: ".." relative: true source: path - version: "0.5.7" + version: "0.6.0" webdriver: dependency: transitive description: diff --git a/lib/src/boringssl/lookup/symbols.generated.dart b/lib/src/boringssl/lookup/symbols.generated.dart index 7dbef621..37f15f5a 100644 --- a/lib/src/boringssl/lookup/symbols.generated.dart +++ b/lib/src/boringssl/lookup/symbols.generated.dart @@ -85,10 +85,12 @@ enum Sym { EVP_CipherUpdate, EVP_DigestFinal, EVP_DigestInit, + EVP_DigestSign, EVP_DigestSignFinal, EVP_DigestSignInit, EVP_DigestSignUpdate, EVP_DigestUpdate, + EVP_DigestVerify, EVP_DigestVerifyFinal, EVP_DigestVerifyInit, EVP_DigestVerifyUpdate, @@ -101,6 +103,7 @@ enum Sym { EVP_parse_public_key, EVP_PKEY_CTX_free, EVP_PKEY_CTX_new, + EVP_PKEY_CTX_new_id, EVP_PKEY_CTX_set0_rsa_oaep_label, EVP_PKEY_CTX_set_rsa_mgf1_md, EVP_PKEY_CTX_set_rsa_oaep_md, @@ -108,13 +111,22 @@ enum Sym { EVP_PKEY_CTX_set_rsa_pss_saltlen, EVP_PKEY_decrypt, EVP_PKEY_decrypt_init, + EVP_PKEY_derive, + EVP_PKEY_derive_init, + EVP_PKEY_derive_set_peer, EVP_PKEY_encrypt, EVP_PKEY_encrypt_init, EVP_PKEY_free, + EVP_PKEY_get_raw_private_key, + EVP_PKEY_get_raw_public_key, EVP_PKEY_get1_EC_KEY, EVP_PKEY_get1_RSA, EVP_PKEY_id, + EVP_PKEY_keygen, + EVP_PKEY_keygen_init, EVP_PKEY_new, + EVP_PKEY_new_raw_private_key, + EVP_PKEY_new_raw_public_key, EVP_PKEY_set1_EC_KEY, EVP_PKEY_set1_RSA, EVP_sha1, @@ -224,10 +236,12 @@ const _SymName = [ 'EVP_CipherUpdate', 'EVP_DigestFinal', 'EVP_DigestInit', + 'EVP_DigestSign', 'EVP_DigestSignFinal', 'EVP_DigestSignInit', 'EVP_DigestSignUpdate', 'EVP_DigestUpdate', + 'EVP_DigestVerify', 'EVP_DigestVerifyFinal', 'EVP_DigestVerifyInit', 'EVP_DigestVerifyUpdate', @@ -240,6 +254,7 @@ const _SymName = [ 'EVP_parse_public_key', 'EVP_PKEY_CTX_free', 'EVP_PKEY_CTX_new', + 'EVP_PKEY_CTX_new_id', 'EVP_PKEY_CTX_set0_rsa_oaep_label', 'EVP_PKEY_CTX_set_rsa_mgf1_md', 'EVP_PKEY_CTX_set_rsa_oaep_md', @@ -247,13 +262,22 @@ const _SymName = [ 'EVP_PKEY_CTX_set_rsa_pss_saltlen', 'EVP_PKEY_decrypt', 'EVP_PKEY_decrypt_init', + 'EVP_PKEY_derive', + 'EVP_PKEY_derive_init', + 'EVP_PKEY_derive_set_peer', 'EVP_PKEY_encrypt', 'EVP_PKEY_encrypt_init', 'EVP_PKEY_free', + 'EVP_PKEY_get_raw_private_key', + 'EVP_PKEY_get_raw_public_key', 'EVP_PKEY_get1_EC_KEY', 'EVP_PKEY_get1_RSA', 'EVP_PKEY_id', + 'EVP_PKEY_keygen', + 'EVP_PKEY_keygen_init', 'EVP_PKEY_new', + 'EVP_PKEY_new_raw_private_key', + 'EVP_PKEY_new_raw_public_key', 'EVP_PKEY_set1_EC_KEY', 'EVP_PKEY_set1_RSA', 'EVP_sha1', diff --git a/lib/src/impl_ffi/impl_ffi.crv25519.dart b/lib/src/impl_ffi/impl_ffi.crv25519.dart new file mode 100644 index 00000000..d53ce949 --- /dev/null +++ b/lib/src/impl_ffi/impl_ffi.crv25519.dart @@ -0,0 +1,132 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +part of 'impl_ffi.dart'; + +const _crv25519KeyLength = 32; + +String _crv25519FromType(int pkeyType) { + switch (pkeyType) { + case EVP_PKEY_X25519: + return 'X25519'; + case EVP_PKEY_ED25519: + return 'Ed25519'; + } + throw operationError('internal error detecting curve'); +} + +_EvpPKey _newRawPrivateKey(int type, Uint8List keyData) { + final crv = _crv25519FromType(type); + + _checkData( + keyData.length == _crv25519KeyLength, + message: '$crv private key should be $_crv25519KeyLength bytes', + ); + + return _Scope.sync((scope) { + final bytes = scope.dataAsPointer(keyData); + final key = ssl.EVP_PKEY_new_raw_private_key( + type, + ffi.nullptr, + bytes, + _crv25519KeyLength, + ); + _checkOp(key.address != 0); + return _EvpPKey.wrap(key); + }); +} + +_EvpPKey _newRawPublicKey(int type, Uint8List keyData) { + final crv = _crv25519FromType(type); + + _checkData( + keyData.length == _crv25519KeyLength, + message: '$crv public key should be $_crv25519KeyLength bytes', + ); + + return _Scope.sync((scope) { + final bytes = scope.dataAsPointer(keyData); + final key = ssl.EVP_PKEY_new_raw_public_key( + type, + ffi.nullptr, + bytes, + _crv25519KeyLength, + ); + _checkOp(key.address != 0); + return _EvpPKey.wrap(key); + }); +} + +_EvpPKey _crv25519ImportJsonWebKey( + int pkeyType, + JsonWebKey jwk, { + required bool isPrivateKey, + required String use, + Set alg = const {}, +}) { + final crv = _crv25519FromType(pkeyType); + + _checkData( + jwk.kty == 'OKP', + message: 'expected an $crv key, JWK property "kty" must be "OKP"', + ); + + _checkData( + jwk.x != null, + message: 'expected an $crv key, JWK property "x" is missing', + ); + + _checkData( + jwk.use == null || jwk.use == use, + message: 'JWK property "use" should be "enc", if present', + ); + + _checkData( + jwk.crv == crv, + message: 'expected an $crv key, JWK property "crv" must be "$crv"', + ); + + final algs = alg.map((e) => '"$e"').join(' or '); + _checkData( + jwk.alg == null || alg.isEmpty || alg.contains(jwk.alg), + message: 'expected an $crv key, JWK property "alg" must be $algs.', + ); + + final x = _jwkDecodeBase64UrlNoPadding(jwk.x!, 'x'); + _checkData( + x.length == _crv25519KeyLength, + message: 'JWK property "x" should be $_crv25519KeyLength bytes', + ); + + if (isPrivateKey) { + _checkData( + jwk.d != null, + message: 'expected an $crv private key, JWK property "d" is missing', + ); + + final d = _jwkDecodeBase64UrlNoPadding(jwk.d!, 'd'); + _checkData( + d.length == _crv25519KeyLength, + message: 'JWK property "d" should be $_crv25519KeyLength bytes', + ); + return _newRawPrivateKey(pkeyType, d); + } + + _checkData( + jwk.d == null, + message: 'expected an $crv public key, JWK property "d" is present', + ); + + return _newRawPublicKey(pkeyType, x); +} diff --git a/lib/src/impl_ffi/impl_ffi.dart b/lib/src/impl_ffi/impl_ffi.dart index 7f985909..985d46ac 100644 --- a/lib/src/impl_ffi/impl_ffi.dart +++ b/lib/src/impl_ffi/impl_ffi.dart @@ -35,8 +35,10 @@ part 'impl_ffi.aescbc.dart'; part 'impl_ffi.aesctr.dart'; part 'impl_ffi.aesgcm.dart'; part 'impl_ffi.digest.dart'; +part 'impl_ffi.crv25519.dart'; part 'impl_ffi.ecdh.dart'; part 'impl_ffi.ecdsa.dart'; +part 'impl_ffi.ed25519.dart'; part 'impl_ffi.hkdf.dart'; part 'impl_ffi.hmac.dart'; part 'impl_ffi.pbkdf2.dart'; @@ -45,6 +47,7 @@ part 'impl_ffi.rsaoaep.dart'; part 'impl_ffi.rsapss.dart'; part 'impl_ffi.rsassapkcs1v15.dart'; part 'impl_ffi.utils.dart'; +part 'impl_ffi.x25519.dart'; part 'impl_ffi.rsa_common.dart'; part 'impl_ffi.ec_common.dart'; part 'impl_ffi.aes_common.dart'; @@ -81,6 +84,12 @@ final class _WebCryptoImpl implements WebCryptoImpl { @override final ecdsaPublicKey = const _StaticEcdsaPublicKeyImpl(); + @override + final ed25519PrivateKey = const _StaticEd25519PrivateKeyImpl(); + + @override + final ed25519PublicKey = const _StaticEd25519PublicKeyImpl(); + @override final rsaOaepPrivateKey = const _StaticRsaOaepPrivateKeyImpl(); @@ -102,6 +111,12 @@ final class _WebCryptoImpl implements WebCryptoImpl { @override final rsaSsaPkcs1v15PublicKey = const _StaticRsaSsaPkcs1V15PublicKeyImpl(); + @override + final x25519PrivateKey = const _StaticX25519PrivateKeyImpl(); + + @override + final x25519PublicKey = const _StaticX25519PublicKeyImpl(); + @override final sha1 = const _Sha1(); diff --git a/lib/src/impl_ffi/impl_ffi.ed25519.dart b/lib/src/impl_ffi/impl_ffi.ed25519.dart new file mode 100644 index 00000000..6dad9c77 --- /dev/null +++ b/lib/src/impl_ffi/impl_ffi.ed25519.dart @@ -0,0 +1,213 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +part of 'impl_ffi.dart'; + +final class _StaticEd25519PrivateKeyImpl + implements StaticEd25519PrivateKeyImpl { + const _StaticEd25519PrivateKeyImpl(); + + @override + Future<(Ed25519PrivateKeyImpl, Ed25519PublicKeyImpl)> generateKey() async { + return _Scope.sync((scope) { + final ctx = scope.create( + () => ssl.EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, ffi.nullptr), + ssl.EVP_PKEY_CTX_free, + ); + final out = scope>(); + _checkOpIsOne(ssl.EVP_PKEY_keygen_init(ctx)); + _checkOpIsOne(ssl.EVP_PKEY_keygen(ctx, out)); + final privKey = _EvpPKey.wrap(out.value); + final rawPubKey = _getRawPublicKey(privKey); + final pubKey = _newRawPublicKey(EVP_PKEY_ED25519, rawPubKey); + return ( + _Ed25519PrivateKeyImpl(privKey), + _Ed25519PublicKeyImpl(pubKey), + ); + }); + } + + @override + Future importJsonWebKey( + Map jwk, + ) async { + final key = _crv25519ImportJsonWebKey( + EVP_PKEY_ED25519, + JsonWebKey.fromJson(jwk), + isPrivateKey: true, + use: 'sig', + alg: {'Ed25519', 'EdDSA'}, + ); + return _Ed25519PrivateKeyImpl(key); + } + + @override + Future importPkcs8Key(List keyData) async { + return _Scope.sync((scope) { + final cbs = scope.createCBS(keyData); + final k = ssl.EVP_parse_private_key(cbs); + _checkOp(k.address != 0); + final key = _EvpPKey.wrap(k); + _checkData(ssl.EVP_PKEY_id.invoke(key) == EVP_PKEY_ED25519, + message: 'key is not an Ed25519 private key'); + return _Ed25519PrivateKeyImpl(key); + }); + } +} + +final class _Ed25519PrivateKeyImpl implements Ed25519PrivateKeyImpl { + final _EvpPKey _key; + + const _Ed25519PrivateKeyImpl(this._key); + + @override + Future> exportJsonWebKey() async { + final x = _getRawPublicKey(_key); + final d = _getRawPrivateKey(_key); + return JsonWebKey( + kty: 'OKP', + crv: 'Ed25519', + alg: 'Ed25519', + x: _jwkEncodeBase64UrlNoPadding(x), + d: _jwkEncodeBase64UrlNoPadding(d), + ).toJson(); + } + + @override + Future exportPkcs8Key() async => _exportPkcs8Key(_key); + + @override + Future signBytes(List data) async { + return _Scope.sync((scope) { + final ctx = scope.create( + ssl.EVP_MD_CTX_new, + ssl.EVP_MD_CTX_free, + ); + _checkOpIsOne(ssl.EVP_DigestSignInit.invoke( + ctx, + ffi.nullptr, + ffi.nullptr, + ffi.nullptr, + _key, + )); + final outLen = scope(); + final bytes = Uint8List.fromList(data); + final pBytes = scope.dataAsPointer(bytes); + _checkOpIsOne(ssl.EVP_DigestSign( + ctx, + ffi.nullptr, + outLen, + pBytes, + bytes.length, + )); + final out = scope(outLen.value); + _checkOpIsOne(ssl.EVP_DigestSign( + ctx, + out, + outLen, + scope.dataAsPointer(bytes), + bytes.length, + )); + return out.copy(outLen.value); + }); + } +} + +final class _StaticEd25519PublicKeyImpl implements StaticEd25519PublicKeyImpl { + const _StaticEd25519PublicKeyImpl(); + + @override + Future importJsonWebKey( + Map jwk) async { + final key = _crv25519ImportJsonWebKey( + EVP_PKEY_ED25519, + JsonWebKey.fromJson(jwk), + isPrivateKey: false, + use: 'sig', + alg: {'Ed25519', 'EdDSA'}, + ); + return _Ed25519PublicKeyImpl(key); + } + + @override + Future importRawKey(List keyData) async { + final key = _newRawPublicKey(EVP_PKEY_ED25519, Uint8List.fromList(keyData)); + return _Ed25519PublicKeyImpl(key); + } + + @override + Future importSpkiKey(List keyData) async { + return _Scope.sync((scope) { + final k = ssl.EVP_parse_public_key(scope.createCBS(keyData)); + _checkData(k.address != 0, fallback: 'unable to parse key'); + final key = _EvpPKey.wrap(k); + _checkData( + ssl.EVP_PKEY_id.invoke(key) == EVP_PKEY_ED25519, + message: 'key is not an Ed25519 public key', + ); + return _Ed25519PublicKeyImpl(key); + }); + } +} + +final class _Ed25519PublicKeyImpl implements Ed25519PublicKeyImpl { + final _EvpPKey _key; + + const _Ed25519PublicKeyImpl(this._key); + + @override + Future> exportJsonWebKey() async { + final x = _getRawPublicKey(_key); + return JsonWebKey( + kty: 'OKP', + crv: 'Ed25519', + alg: 'Ed25519', + x: _jwkEncodeBase64UrlNoPadding(x), + ).toJson(); + } + + @override + Future exportRawKey() async => _getRawPublicKey(_key); + + @override + Future exportSpkiKey() async => _exportSpkiKey(_key); + + @override + Future verifyBytes(List signature, List data) async { + return _Scope.sync((scope) { + final ctx = scope.create( + ssl.EVP_MD_CTX_new, + ssl.EVP_MD_CTX_free, + ); + _checkOpIsOne(ssl.EVP_DigestVerifyInit.invoke( + ctx, + ffi.nullptr, + ffi.nullptr, + ffi.nullptr, + _key, + )); + final verified = ssl.EVP_DigestVerify( + ctx, + scope.dataAsPointer(signature), + signature.length, + scope.dataAsPointer(data), + data.length, + ); + if (verified != 1) { + ssl.ERR_clear_error(); + } + return verified == 1; + }); + } +} diff --git a/lib/src/impl_ffi/impl_ffi.utils.dart b/lib/src/impl_ffi/impl_ffi.utils.dart index 324d2cb4..4ff2d4a0 100644 --- a/lib/src/impl_ffi/impl_ffi.utils.dart +++ b/lib/src/impl_ffi/impl_ffi.utils.dart @@ -65,6 +65,24 @@ extension on T Function(ffi.Pointer, A1) { T invoke(_EvpPKey key, A1 arg1) => key.use((pkey) => this(pkey, arg1)); } +/// Extension of native function that takes a [EVP_PKEY], making it easy to call +/// using a wrapped [_EvpPKey]. +extension on T Function(ffi.Pointer, A1, A2) { + /// Invoke this function with unwrapped [key]. + T invoke( + _EvpPKey key, + A1 arg1, + A2 arg2, + ) => + key.use( + (pkey) => this( + pkey, + arg1, + arg2, + ), + ); +} + /// Extension of native function that takes a [EVP_PKEY], making it easy to call /// using a wrapped [_EvpPKey]. extension on T Function(A1, ffi.Pointer) { @@ -517,3 +535,29 @@ String _jwkEncodeBase64UrlNoPadding(List data) { } return padded.substring(0, i); } + +/// Gets the raw private [key] bytes. +Uint8List _getRawPrivateKey(_EvpPKey key) { + return _Scope.sync((scope) { + final len = scope(); + _checkOpIsOne( + ssl.EVP_PKEY_get_raw_private_key.invoke(key, ffi.nullptr, len), + ); + final out = scope(len.value); + _checkOpIsOne(ssl.EVP_PKEY_get_raw_private_key.invoke(key, out, len)); + return out.copy(len.value); + }); +} + +/// Gets the public [key] bytes. +Uint8List _getRawPublicKey(_EvpPKey key) { + return _Scope.sync((scope) { + final len = scope(); + _checkOpIsOne( + ssl.EVP_PKEY_get_raw_public_key.invoke(key, ffi.nullptr, len), + ); + final out = scope(len.value); + _checkOpIsOne(ssl.EVP_PKEY_get_raw_public_key.invoke(key, out, len)); + return out.copy(len.value); + }); +} diff --git a/lib/src/impl_ffi/impl_ffi.x25519.dart b/lib/src/impl_ffi/impl_ffi.x25519.dart new file mode 100644 index 00000000..1f1f7e46 --- /dev/null +++ b/lib/src/impl_ffi/impl_ffi.x25519.dart @@ -0,0 +1,196 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ignore_for_file: non_constant_identifier_names + +part of 'impl_ffi.dart'; + +final class _StaticX25519PrivateKeyImpl implements StaticX25519PrivateKeyImpl { + const _StaticX25519PrivateKeyImpl(); + + @override + Future<(X25519PrivateKeyImpl, X25519PublicKeyImpl)> generateKey() async { + return _Scope.sync((scope) { + final ctx = scope.create( + () => ssl.EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, ffi.nullptr), + ssl.EVP_PKEY_CTX_free, + ); + final out = scope>(); + _checkOpIsOne(ssl.EVP_PKEY_keygen_init(ctx)); + _checkOpIsOne(ssl.EVP_PKEY_keygen(ctx, out)); + final privKey = _EvpPKey.wrap(out.value); + final pubKey = _newRawPublicKey( + EVP_PKEY_X25519, + _getRawPublicKey(privKey), + ); + return ( + _X25519PrivateKeyImpl(privKey), + _X25519PublicKeyImpl(pubKey), + ); + }); + } + + @override + Future importJsonWebKey( + Map jwk) async { + final key = _crv25519ImportJsonWebKey( + EVP_PKEY_X25519, + JsonWebKey.fromJson(jwk), + isPrivateKey: true, + use: 'enc', + ); + return _X25519PrivateKeyImpl(key); + } + + @override + Future importPkcs8Key(List keyData) async { + return _Scope.sync((scope) { + final k = ssl.EVP_parse_private_key(scope.createCBS(keyData)); + _checkData(k.address != 0, fallback: 'unable to parse key'); + final key = _EvpPKey.wrap(k); + _checkData(ssl.EVP_PKEY_id.invoke(key) == EVP_PKEY_X25519, + message: 'key is not an X25519 key'); + return _X25519PrivateKeyImpl(key); + }); + } +} + +final class _StaticX25519PublicKeyImpl implements StaticX25519PublicKeyImpl { + const _StaticX25519PublicKeyImpl(); + + @override + Future importJsonWebKey(Map jwk) async { + final key = _crv25519ImportJsonWebKey( + EVP_PKEY_X25519, + JsonWebKey.fromJson(jwk), + isPrivateKey: false, + use: 'enc', + ); + return _X25519PublicKeyImpl(key); + } + + @override + Future importRawKey(List keyData) async { + final raw = Uint8List.fromList(keyData); + final key = _newRawPublicKey(EVP_PKEY_X25519, raw); + return _X25519PublicKeyImpl(key); + } + + @override + Future importSpkiKey(List keyData) async { + return _Scope.sync((scope) { + final k = ssl.EVP_parse_public_key(scope.createCBS(keyData)); + _checkData(k.address != 0, fallback: 'unable to parse key'); + final key = _EvpPKey.wrap(k); + _checkData( + ssl.EVP_PKEY_id.invoke(key) == EVP_PKEY_X25519, + message: 'key is not an X25519 public key', + ); + return _X25519PublicKeyImpl(key); + }); + } +} + +final class _X25519PrivateKeyImpl implements X25519PrivateKeyImpl { + final _EvpPKey _key; + + _X25519PrivateKeyImpl(this._key); + + @override + Future deriveBits( + int length, + X25519PublicKeyImpl publicKey, + ) async { + if (publicKey is! _X25519PublicKeyImpl) { + throw ArgumentError.value( + publicKey, + 'publicKey', + 'custom implementations of X25519PublicKeyImpl is not supported', + ); + } + + if (length <= 0) { + throw ArgumentError.value(length, 'length', 'must be positive'); + } + + const maxLength = _crv25519KeyLength * 8; + if (length > maxLength) { + throw operationError( + 'Length in X25519 key derivation is too large. ' + 'Maximum allowed is $maxLength bits.', + ); + } + + if (length == 0) { + return Uint8List(0); + } + + return _Scope.sync((scope) { + final ctx = scope.create( + () => ssl.EVP_PKEY_CTX_new.invoke(_key, ffi.nullptr), + ssl.EVP_PKEY_CTX_free, + ); + final out = scope(_crv25519KeyLength); + final outLen = scope(); + outLen.value = _crv25519KeyLength; + _checkOpIsOne(ssl.EVP_PKEY_derive_init(ctx)); + _checkOpIsOne(ssl.EVP_PKEY_derive_set_peer.invoke(ctx, publicKey._key)); + _checkOpIsOne(ssl.EVP_PKEY_derive(ctx, out, outLen)); + _checkOp(outLen.value == _crv25519KeyLength); + Uint8List derived = out.copy(_crv25519KeyLength); + + final lengthBytes = (length / 8).ceil(); + derived = derived.sublist(0, lengthBytes); + final zeroBits = lengthBytes * 8 - length; + if (zeroBits > 0) { + derived.last &= ((0xff << zeroBits) & 0xff); + } + return derived; + }); + } + + @override + Future> exportJsonWebKey() async { + return JsonWebKey( + kty: 'OKP', + crv: 'X25519', + x: _jwkEncodeBase64UrlNoPadding(_getRawPublicKey(_key)), + d: _jwkEncodeBase64UrlNoPadding(_getRawPrivateKey(_key)), + ).toJson(); + } + + @override + Future exportPkcs8Key() async => _exportPkcs8Key(_key); +} + +final class _X25519PublicKeyImpl implements X25519PublicKeyImpl { + final _EvpPKey _key; + + const _X25519PublicKeyImpl(this._key); + + @override + Future> exportJsonWebKey() async { + return JsonWebKey( + kty: 'OKP', + crv: 'X25519', + x: _jwkEncodeBase64UrlNoPadding(_getRawPublicKey(_key)), + ).toJson(); + } + + @override + Future exportRawKey() async => _getRawPublicKey(_key); + + @override + Future exportSpkiKey() async => _exportSpkiKey(_key); +} diff --git a/lib/src/impl_interface/impl_interface.dart b/lib/src/impl_interface/impl_interface.dart index 845a177f..ee5d341d 100644 --- a/lib/src/impl_interface/impl_interface.dart +++ b/lib/src/impl_interface/impl_interface.dart @@ -26,10 +26,12 @@ part 'impl_interface.pbkdf2.dart'; part 'impl_interface.aesgcm.dart'; part 'impl_interface.ecdh.dart'; part 'impl_interface.ecdsa.dart'; +part 'impl_interface.ed25519.dart'; part 'impl_interface.rsaoaep.dart'; part 'impl_interface.hkdf.dart'; part 'impl_interface.rsapss.dart'; part 'impl_interface.rsassapkcs1v15.dart'; +part 'impl_interface.x25519.dart'; part 'impl_interface.digest.dart'; part 'impl_interface.random.dart'; @@ -92,6 +94,8 @@ abstract interface class WebCryptoImpl { StaticEcdhPublicKeyImpl get ecdhPublicKey; StaticEcdsaPrivateKeyImpl get ecdsaPrivateKey; StaticEcdsaPublicKeyImpl get ecdsaPublicKey; + StaticEd25519PrivateKeyImpl get ed25519PrivateKey; + StaticEd25519PublicKeyImpl get ed25519PublicKey; StaticRsaOaepPrivateKeyImpl get rsaOaepPrivateKey; StaticRsaOaepPublicKeyImpl get rsaOaepPublicKey; StaticHkdfSecretKeyImpl get hkdfSecretKey; @@ -99,6 +103,8 @@ abstract interface class WebCryptoImpl { StaticRsaPssPublicKeyImpl get rsaPssPublicKey; StaticRsaSsaPkcs1v15PrivateKeyImpl get rsaSsaPkcs1v15PrivateKey; StaticRsaSsaPkcs1v15PublicKeyImpl get rsaSsaPkcs1v15PublicKey; + StaticX25519PrivateKeyImpl get x25519PrivateKey; + StaticX25519PublicKeyImpl get x25519PublicKey; HashImpl get sha1; HashImpl get sha256; HashImpl get sha384; diff --git a/lib/src/impl_interface/impl_interface.ed25519.dart b/lib/src/impl_interface/impl_interface.ed25519.dart new file mode 100644 index 00000000..88cc7acd --- /dev/null +++ b/lib/src/impl_interface/impl_interface.ed25519.dart @@ -0,0 +1,40 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +part of 'impl_interface.dart'; + +abstract interface class StaticEd25519PrivateKeyImpl { + Future importPkcs8Key(List keyData); + Future importJsonWebKey(Map jwk); + Future<(Ed25519PrivateKeyImpl, Ed25519PublicKeyImpl)> generateKey(); +} + +abstract interface class Ed25519PrivateKeyImpl { + Future signBytes(List data); + Future exportPkcs8Key(); + Future> exportJsonWebKey(); +} + +abstract interface class StaticEd25519PublicKeyImpl { + Future importRawKey(List keyData); + Future importJsonWebKey(Map jwk); + Future importSpkiKey(List keyData); +} + +abstract interface class Ed25519PublicKeyImpl { + Future verifyBytes(List signature, List data); + Future exportRawKey(); + Future exportSpkiKey(); + Future> exportJsonWebKey(); +} diff --git a/lib/src/impl_interface/impl_interface.x25519.dart b/lib/src/impl_interface/impl_interface.x25519.dart new file mode 100644 index 00000000..da0fd1b5 --- /dev/null +++ b/lib/src/impl_interface/impl_interface.x25519.dart @@ -0,0 +1,39 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +part of 'impl_interface.dart'; + +abstract interface class StaticX25519PrivateKeyImpl { + Future importPkcs8Key(List keyData); + Future importJsonWebKey(Map jwk); + Future<(X25519PrivateKeyImpl, X25519PublicKeyImpl)> generateKey(); +} + +abstract interface class X25519PrivateKeyImpl { + Future deriveBits(int length, X25519PublicKeyImpl publicKey); + Future exportPkcs8Key(); + Future> exportJsonWebKey(); +} + +abstract interface class StaticX25519PublicKeyImpl { + Future importRawKey(List keyData); + Future importSpkiKey(List keyData); + Future importJsonWebKey(Map jwk); +} + +abstract interface class X25519PublicKeyImpl { + Future exportRawKey(); + Future exportSpkiKey(); + Future> exportJsonWebKey(); +} diff --git a/lib/src/impl_js/impl_js.dart b/lib/src/impl_js/impl_js.dart index 7e4f0bd8..5a2be9ed 100644 --- a/lib/src/impl_js/impl_js.dart +++ b/lib/src/impl_js/impl_js.dart @@ -27,6 +27,7 @@ part 'impl_js.aesgcm.dart'; part 'impl_js.digest.dart'; part 'impl_js.ecdh.dart'; part 'impl_js.ecdsa.dart'; +part 'impl_js.ed25519.dart'; part 'impl_js.hkdf.dart'; part 'impl_js.hmac.dart'; part 'impl_js.pbkdf2.dart'; @@ -34,6 +35,7 @@ part 'impl_js.random.dart'; part 'impl_js.rsaoaep.dart'; part 'impl_js.rsapss.dart'; part 'impl_js.rsassapkcs1v15.dart'; +part 'impl_js.x25519.dart'; part 'impl_js.utils.dart'; const WebCryptoImpl webCryptImpl = _WebCryptoImpl(); @@ -68,6 +70,12 @@ final class _WebCryptoImpl implements WebCryptoImpl { @override final ecdsaPublicKey = const _StaticEcdsaPublicKeyImpl(); + @override + final ed25519PrivateKey = const _StaticEd25519PrivateKeyImpl(); + + @override + final ed25519PublicKey = const _StaticEd25519PublicKeyImpl(); + @override final rsaOaepPrivateKey = const _StaticRsaOaepPrivateKeyImpl(); @@ -89,6 +97,12 @@ final class _WebCryptoImpl implements WebCryptoImpl { @override final rsaSsaPkcs1v15PublicKey = const _StaticRsaSsaPkcs1V15PublicKeyImpl(); + @override + final x25519PrivateKey = const _StaticX25519PrivateKeyImpl(); + + @override + final x25519PublicKey = const _StaticX25519PublicKeyImpl(); + @override final sha1 = const _HashImpl('SHA-1'); diff --git a/lib/src/impl_js/impl_js.ed25519.dart b/lib/src/impl_js/impl_js.ed25519.dart new file mode 100644 index 00000000..118feff0 --- /dev/null +++ b/lib/src/impl_js/impl_js.ed25519.dart @@ -0,0 +1,157 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +part of 'impl_js.dart'; + +const _ed25519Algorithm = subtle.Algorithm(name: 'Ed25519'); + +final class _StaticEd25519PrivateKeyImpl + implements StaticEd25519PrivateKeyImpl { + const _StaticEd25519PrivateKeyImpl(); + + @override + Future<(Ed25519PrivateKeyImpl, Ed25519PublicKeyImpl)> generateKey() async { + final kp = await _generateKeyPair( + _ed25519Algorithm, + _usagesSignVerify, + ); + return ( + _Ed25519PrivateKeyImpl(kp.privateKey), + _Ed25519PublicKeyImpl(kp.publicKey), + ); + } + + @override + Future importJsonWebKey( + Map jwk, + ) async { + return _Ed25519PrivateKeyImpl( + await _importJsonWebKey(jwk, _ed25519Algorithm, ['sign'], 'private'), + ); + } + + @override + Future importPkcs8Key(List keyData) async { + return _Ed25519PrivateKeyImpl( + await _importKey( + 'pkcs8', + keyData, + _ed25519Algorithm, + _usagesSign, + 'private', + ), + ); + } +} + +final class _Ed25519PrivateKeyImpl implements Ed25519PrivateKeyImpl { + final subtle.JSCryptoKey _key; + + _Ed25519PrivateKeyImpl(this._key); + + @override + String toString() { + return 'Instance of \'Ed25519PrivateKeyImpl\''; + } + + @override + Future> exportJsonWebKey() => _exportJsonWebKey(_key); + + @override + Future exportPkcs8Key() => _exportKey('pkcs8', _key); + + @override + Future signBytes(List data) async { + final sig = await subtle.sign( + _ed25519Algorithm, + _key, + Uint8List.fromList(data), + ); + return sig.asUint8List(); + } +} + +final class _StaticEd25519PublicKeyImpl implements StaticEd25519PublicKeyImpl { + const _StaticEd25519PublicKeyImpl(); + + @override + Future importJsonWebKey( + Map jwk, + ) async { + return _Ed25519PublicKeyImpl( + await _importJsonWebKey( + jwk, + _ed25519Algorithm, + _usagesVerify, + 'public', + ), + ); + } + + @override + Future importRawKey(List keyData) async { + return _Ed25519PublicKeyImpl( + await _importKey( + 'raw', + keyData, + _ed25519Algorithm, + _usagesVerify, + 'public', + ), + ); + } + + @override + Future importSpkiKey(List keyData) async { + return _Ed25519PublicKeyImpl( + await _importKey( + 'spki', + keyData, + _ed25519Algorithm, + _usagesVerify, + 'public', + ), + ); + } +} + +final class _Ed25519PublicKeyImpl implements Ed25519PublicKeyImpl { + final subtle.JSCryptoKey _key; + + _Ed25519PublicKeyImpl(this._key); + + @override + String toString() { + return 'Instance of \'Ed25519PublicKeyImpl\''; + } + + @override + Future> exportJsonWebKey() => _exportJsonWebKey(_key); + + @override + Future exportRawKey() => _exportKey('raw', _key); + + @override + Future exportSpkiKey() => _exportKey('spki', _key); + + @override + Future verifyBytes(List signature, List data) async { + return await subtle.verify( + _ed25519Algorithm, + _key, + Uint8List.fromList(signature), + Uint8List.fromList(data), + ); + } +} diff --git a/lib/src/impl_js/impl_js.x25519.dart b/lib/src/impl_js/impl_js.x25519.dart new file mode 100644 index 00000000..06fff55c --- /dev/null +++ b/lib/src/impl_js/impl_js.x25519.dart @@ -0,0 +1,171 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ignore_for_file: non_constant_identifier_names + +part of 'impl_js.dart'; + +const _x25519Algorithm = subtle.Algorithm(name: 'X25519'); + +final class _StaticX25519PrivateKeyImpl implements StaticX25519PrivateKeyImpl { + const _StaticX25519PrivateKeyImpl(); + + @override + Future<(X25519PrivateKeyImpl, X25519PublicKeyImpl)> generateKey() async { + final kp = await _generateKeyPair( + _x25519Algorithm, + _usagesDeriveBits, + ); + return ( + _X25519PrivateKeyImpl(kp.privateKey), + _X25519PublicKeyImpl(kp.publicKey), + ); + } + + @override + Future importJsonWebKey( + Map jwk, + ) async { + return _X25519PrivateKeyImpl(await _importJsonWebKey( + jwk, + _x25519Algorithm, + _usagesDeriveBits, + 'private', + )); + } + + @override + Future importPkcs8Key(List keyData) async { + return _X25519PrivateKeyImpl( + await _importKey( + 'pkcs8', + keyData, + _x25519Algorithm, + _usagesDeriveBits, + 'private', + ), + ); + } +} + +final class _X25519PrivateKeyImpl implements X25519PrivateKeyImpl { + final subtle.JSCryptoKey _key; + + _X25519PrivateKeyImpl(this._key); + + @override + String toString() { + return 'Instance of \'X25519PrivateKeyImpl\''; + } + + @override + Future deriveBits( + int length, + X25519PublicKeyImpl publicKey, + ) async { + if (publicKey is! _X25519PublicKeyImpl) { + throw ArgumentError.value( + publicKey, + 'publicKey', + 'custom implementations of X25519PublicKeyImpl is not supported', + ); + } + final lengthInBytes = (length / 8).ceil(); + final derived = await _deriveBits( + subtle.Algorithm( + name: _x25519Algorithm.name, + public: publicKey._key, + ), + _key, + lengthInBytes * 8, + invalidAccessErrorIsArgumentError: true, + ); + + // Only return the first [length] bits from derived. + final zeroBits = lengthInBytes * 8 - length; + assert(zeroBits < 8); + if (zeroBits > 0) { + derived.last &= ((0xff << zeroBits) & 0xff); + } + return derived; + } + + @override + Future> exportJsonWebKey() => _exportJsonWebKey(_key); + + @override + Future exportPkcs8Key() => _exportKey('pkcs8', _key); +} + +final class _StaticX25519PublicKeyImpl implements StaticX25519PublicKeyImpl { + const _StaticX25519PublicKeyImpl(); + + @override + Future importJsonWebKey(Map jwk) async { + return _X25519PublicKeyImpl( + await _importJsonWebKey( + jwk, + _x25519Algorithm, + [], + 'public', + ), + ); + } + + @override + Future importRawKey(List keyData) async { + return _X25519PublicKeyImpl( + await _importKey( + 'raw', + keyData, + _x25519Algorithm, + [], + 'public', + ), + ); + } + + @override + Future importSpkiKey(List keyData) async { + return _X25519PublicKeyImpl( + await _importKey( + 'spki', + keyData, + _x25519Algorithm, + [], + 'public', + ), + ); + } +} + +final class _X25519PublicKeyImpl implements X25519PublicKeyImpl { + final subtle.JSCryptoKey _key; + + _X25519PublicKeyImpl(this._key); + + @override + String toString() { + return 'Instance of \'X25519PublicKeyImpl\''; + } + + @override + Future> exportJsonWebKey() => _exportJsonWebKey(_key); + + @override + Future exportRawKey() => _exportKey('raw', _key); + + @override + Future exportSpkiKey() => _exportKey('spki', _key); +} diff --git a/lib/src/impl_stub/impl_stub.dart b/lib/src/impl_stub/impl_stub.dart index c0d24b51..d69bbe55 100644 --- a/lib/src/impl_stub/impl_stub.dart +++ b/lib/src/impl_stub/impl_stub.dart @@ -25,10 +25,12 @@ part 'impl_stub.hmac.dart'; part 'impl_stub.pbkdf2.dart'; part 'impl_stub.ecdh.dart'; part 'impl_stub.ecdsa.dart'; +part 'impl_stub.ed25519.dart'; part 'impl_stub.rsaoaep.dart'; part 'impl_stub.hkdf.dart'; part 'impl_stub.rsapss.dart'; part 'impl_stub.rsassapkcs1v15.dart'; +part 'impl_stub.x25519.dart'; part 'impl_stub.digest.dart'; part 'impl_stub.random.dart'; @@ -64,6 +66,12 @@ final class _WebCryptoImpl implements WebCryptoImpl { @override final ecdsaPublicKey = const _StaticEcdsaPublicKeyImpl(); + @override + final ed25519PrivateKey = const _StaticEd25519PrivateKeyImpl(); + + @override + final ed25519PublicKey = const _StaticEd25519PublicKeyImpl(); + @override final rsaOaepPrivateKey = const _StaticRsaOaepPrivateKeyImpl(); @@ -85,6 +93,12 @@ final class _WebCryptoImpl implements WebCryptoImpl { @override final rsaSsaPkcs1v15PublicKey = const _StaticRsaSsaPkcs1V15PublicKeyImpl(); + @override + final x25519PrivateKey = const _StaticX25519PrivateKeyImpl(); + + @override + final x25519PublicKey = const _StaticX25519PublicKeyImpl(); + @override final sha1 = const _HashImpl(); diff --git a/lib/src/impl_stub/impl_stub.ed25519.dart b/lib/src/impl_stub/impl_stub.ed25519.dart new file mode 100644 index 00000000..6605716f --- /dev/null +++ b/lib/src/impl_stub/impl_stub.ed25519.dart @@ -0,0 +1,58 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +part of 'impl_stub.dart'; + +final class _StaticEd25519PrivateKeyImpl + implements StaticEd25519PrivateKeyImpl { + const _StaticEd25519PrivateKeyImpl(); + + @override + Future importPkcs8Key( + List keyData, + ) => + throw UnimplementedError('Not implemented'); + + @override + Future importJsonWebKey( + Map jwk, + ) => + throw UnimplementedError('Not implemented'); + + @override + Future<(Ed25519PrivateKeyImpl, Ed25519PublicKeyImpl)> generateKey() => + throw UnimplementedError('Not implemented'); +} + +final class _StaticEd25519PublicKeyImpl implements StaticEd25519PublicKeyImpl { + const _StaticEd25519PublicKeyImpl(); + + @override + Future importRawKey( + List keyData, + ) => + throw UnimplementedError('Not implemented'); + + @override + Future importJsonWebKey( + Map jwk, + ) => + throw UnimplementedError('Not implemented'); + + @override + Future importSpkiKey( + List keyData, + ) => + throw UnimplementedError('Not implemented'); +} diff --git a/lib/src/impl_stub/impl_stub.x25519.dart b/lib/src/impl_stub/impl_stub.x25519.dart new file mode 100644 index 00000000..547a2e7f --- /dev/null +++ b/lib/src/impl_stub/impl_stub.x25519.dart @@ -0,0 +1,47 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +part of 'impl_stub.dart'; + +final class _StaticX25519PrivateKeyImpl implements StaticX25519PrivateKeyImpl { + const _StaticX25519PrivateKeyImpl(); + + @override + Future importPkcs8Key(List keyData) => + throw UnimplementedError('Not implemented'); + + @override + Future importJsonWebKey(Map jwk) => + throw UnimplementedError('Not implemented'); + + @override + Future<(X25519PrivateKeyImpl, X25519PublicKeyImpl)> generateKey() => + throw UnimplementedError('Not implemented'); +} + +final class _StaticX25519PublicKeyImpl implements StaticX25519PublicKeyImpl { + const _StaticX25519PublicKeyImpl(); + + @override + Future importRawKey(List keyData) => + throw UnimplementedError('Not implemented'); + + @override + Future importSpkiKey(List keyData) => + throw UnimplementedError('Not implemented'); + + @override + Future importJsonWebKey(Map jwk) => + throw UnimplementedError('Not implemented'); +} diff --git a/lib/src/testing/testing.dart b/lib/src/testing/testing.dart index bf99f9f3..28a5d0e0 100644 --- a/lib/src/testing/testing.dart +++ b/lib/src/testing/testing.dart @@ -26,6 +26,8 @@ import 'webcrypto/pbkdf2.dart' as pbkdf2; import 'webcrypto/rsaoaep.dart' as rsaoaep; import 'webcrypto/rsapss.dart' as rsapss; import 'webcrypto/rsassapkcs1v15.dart' as rsassapkcs1v15; +import 'webcrypto/x25519.dart' as x25519; +import 'webcrypto/ed25519.dart' as ed25519; // Other test files, that don't use TestRunner import 'webcrypto/random.dart' as random; @@ -34,17 +36,19 @@ import 'webcrypto/digest.dart' as digest; /// Test runners from all test files except `digest.dart` and /// `random.dart`, which do not use [TestRunner]. final _testRunners = [ - aescbc.runner, - aesctr.runner, - aesgcm.runner, - ecdh.runner, - ecdsa.runner, - hkdf.runner, - hmac.runner, - pbkdf2.runner, - rsaoaep.runner, - rsapss.runner, - rsassapkcs1v15.runner, + // aescbc.runner, + // aesctr.runner, + // aesgcm.runner, + // ecdh.runner, + // ecdsa.runner, + // hkdf.runner, + // hmac.runner, + // pbkdf2.runner, + // rsaoaep.runner, + // rsapss.runner, + // rsassapkcs1v15.runner, + ed25519.runner, + x25519.runner, ]; /// Utility function that runs all tests using [testFn]. diff --git a/lib/src/testing/webcrypto/ed25519.dart b/lib/src/testing/webcrypto/ed25519.dart new file mode 100644 index 00000000..38a1b359 --- /dev/null +++ b/lib/src/testing/webcrypto/ed25519.dart @@ -0,0 +1,83 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:webcrypto/webcrypto.dart'; +import '../utils/utils.dart'; +import '../utils/testrunner.dart'; + +final runner = TestRunner.asymmetric( + algorithm: 'Ed25519', + importPrivateRawKey: null, // not supported + exportPrivateRawKey: null, + importPrivatePkcs8Key: (keyData, _) => + Ed25519PrivateKey.importPkcs8Key(keyData), + exportPrivatePkcs8Key: (key) => key.exportPkcs8Key(), + importPrivateJsonWebKey: (jsonWebKeyData, _) => + Ed25519PrivateKey.importJsonWebKey(jsonWebKeyData), + exportPrivateJsonWebKey: (key) => key.exportJsonWebKey(), + importPublicRawKey: (keyData, _) => Ed25519PublicKey.importRawKey(keyData), + exportPublicRawKey: (key) => key.exportRawKey(), + importPublicSpkiKey: (keyData, _) => Ed25519PublicKey.importSpkiKey(keyData), + exportPublicSpkiKey: (key) => key.exportSpkiKey(), + importPublicJsonWebKey: (jsonWebKeyData, _) => + Ed25519PublicKey.importJsonWebKey(jsonWebKeyData), + exportPublicJsonWebKey: (key) => key.exportJsonWebKey(), + generateKeyPair: (_) => Ed25519PrivateKey.generateKey(), + signBytes: (key, data, _) => key.signBytes(data), + verifyBytes: (key, signature, data, _) => key.verifyBytes(signature, data), + testData: _testData, +); + +void main() async { + log('generate Ed25519 test case'); + await runner.generate( + generateKeyParams: {}, + importKeyParams: {}, + ); + log('--------------------'); + + await runner.tests().runTests(); +} + +// Allow single quotes for hardcoded testData written as JSON: +// ignore_for_file: prefer_single_quotes +final _testData = [ + { + "name": "generated on boringssl/mac at 2025-10-10T14:38:47", + "privatePkcs8KeyData": + "MC4CAQAwBQYDK2VwBCIEIBEI/GyQ7Xh+wcPtrvvY2K9QaLU3cDVa5hCMrZW4bIVP", + "privateJsonWebKeyData": { + "kty": "OKP", + "alg": "Ed25519", + "crv": "Ed25519", + "x": "ikNZOCnJK6CDQn2NRicyWxafv-LUqZtmjDEx_s9pezE", + "d": "EQj8bJDteH7Bw-2u-9jYr1BotTdwNVrmEIytlbhshU8" + }, + "publicRawKeyData": "ikNZOCnJK6CDQn2NRicyWxafv+LUqZtmjDEx/s9pezE=", + "publicSpkiKeyData": + "MCowBQYDK2VwAyEAikNZOCnJK6CDQn2NRicyWxafv+LUqZtmjDEx/s9pezE=", + "publicJsonWebKeyData": { + "kty": "OKP", + "alg": "Ed25519", + "crv": "Ed25519", + "x": "ikNZOCnJK6CDQn2NRicyWxafv-LUqZtmjDEx_s9pezE" + }, + "plaintext": + "bnQgc29kYWxlcyBuZXF1ZSBpbiBpcHN1bSB0ZW1wb3IsIGV0IGZhdWNpYnVzIHR1cnBpcyB0cmlzdGlxdWUuIFNlZCBub24KcGxhY2VyYXQgZXJhdC4gU2VkIHF1YW0gdHVycGlzLCB0ZW1wdXMgaW4gYW50ZSBub24sIHNvZGFsZXMgc2NlbGVyaXNxdWUgcXVhbS4KQWxpcXVhbSB2aXRhZSBzYWdpdHRpcyBmZWxpcy4gT3JjaSB2YXJpdXMgbmF0b3F1ZSBwZW5hdGlidXMgZXQgbWFnbmlzIGRpcwpwYXJ0dXJpZW50IG1vbnRlcywgbmFzY2V0dXIgcmlkaWN1bHVzIG11cy4gTnVsbGFtIHRlbXBvciBlcmF0IG5vbiBibGFuZGl0CmVsZW1lbnR1bS4gUGhhc2VsbHVzIHZlbCBkaWFtIGZlbGlzLiBQcmFlc2VudCBmZXJtZW50dW0gZXJhdCB2aXRhZSBsaWd1bGEKcHJldGl1bSBpbXBlcmRpZXQuIFByb2luIGluIGxhY2luaWEgZXguIFNlZCBmZXVnaWF0IGVnZXN0YXMgbHVjdHVzLiBDcmFzCnBlbGxlbnRlc3F1ZSBvcmNpIHF1aXMgbWkgYXVjdG9yIGNvbW1vZG8uIEFlbmVhbiBzaXQgYW1ldCBsdWN0dXMgbGliZXJvLiBNb3JiaQpldSBlbGl0IHNlZCBsaWd1bGEgaGVuZHJlcml0IHN1c2NpcGl0LiBQcmFlc2VudCBmYWNpbGlzaXMgbmlzbCBhIG1hdXJpcyBjdXJzdXMKbW9sZXN0aWUuCgpQZWxsZW50ZXNxdWUgaGFiaXRhbnQgbW9yYmkgdHJpc3RpcXVlIHNlbmVjdHVzIGV0IG5ldHVzIGV0IG1hbGVzdWFkYSBmYW1lcyBhYwp0dXJwaXMgZWdlc3Rhcy4gQ3JhcyBsYWNpbmlhIGFudGUgYWMgbGFvcmVldCBzZW1wZXIuIE1vcmJpIGlhY3VsaXMgbG9ib3J0aXMKbWFzc2EsIHF1aXMgcGhhcmV0cmEgcHVydXMgY29tbW9kbyB2ZWwuIE51bGxhIHJob25jdXMgZW5pbSBuaWJoLCBhYyB1bHRyaWNlcwpsZWN0dXMgZWxlbWVudHVtIHV0LiBQcm9pbiBwZWxsZW50ZXNxdWUgbWF4aW11cyBldWlzbW9kLiBRdWlzcXVlIGNvbnZhbGxpcyBkdWkKYWMgbG9yZW0gc29kYWxlcyBwbGFjZXJhdC4gTnVuYyBhbGlxdWV0IHB1cnVzIGF0IGVyb3MgZWxlbWVudHVtIHNjZWxlcmlzcXVlIHF1aXMKZXQgbmVxdWUuIFZlc3RpYnVsdW0gbm9uIHB1cnVzIGludGVyZHVtLCB2b2x1dHBhdCBleCBzZWQsIHBsYWNlcmF0IGVsaXQuIE51bGxhbQpuZWMgdmVsaXQgdnVscHV0YXRlLCBzZW1wZXIgZG9sb3IgdXQsIG1hdHRpcyBwdXJ1cy4gTWFlY2VuYXMgYXQgaWFjdWxpcyBhdWd1ZS4KRG9uZWMgdGVtcG9yIG5pc2wgZXUgZmluaWJ1cyBjb25ndWUuCgpMb3JlbSBpcHN1bSBkb2xvciBzaXQgYW1ldCwgY29uc2VjdGV0dXIgYWRpcGlzY2luZyBlbGl0LiBEb25lYyBpbiBsb3JlbQppbXBlcmRpZXQsIGVsZWlmZW5kIGxvcmVtIGltcGVyZGlldCwgcG9ydGEgZXJhdC4gVmVzdGlidWx1bSBpbiBwb3J0dGl0b3IgdGVsbHVzLgpBZW5lYW4gZGljdHVtIGRhcGlidXMgbWFnbmEsIHZlbCB2ZW5lbmF0aXMgc2FwaWVuIHBvc3VlcmUgYXQuIEV0aWFtIGV1IGhlbmRyZXJpdApsYWN1cywgbmVjIHBvc3VlcmUgbGliZXJvLiBBZW5lYW4=", + "signature": + "SjQ/uXLEj5Y7/UsixNmKJov8zUCjzB4sXoRPgt/BHkt9BFkOY/69mE5h6/tykXOJ+i53TArW+JZxGCynswK1DA==", + "importKeyParams": {}, + "signVerifyParams": {}, + }, +]; diff --git a/lib/src/testing/webcrypto/x25519.dart b/lib/src/testing/webcrypto/x25519.dart new file mode 100644 index 00000000..a70e832c --- /dev/null +++ b/lib/src/testing/webcrypto/x25519.dart @@ -0,0 +1,93 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:webcrypto/webcrypto.dart'; +import '../utils/utils.dart'; +import '../utils/testrunner.dart'; + +final runner = TestRunner.asymmetric( + algorithm: 'X25519', + importPrivateRawKey: null, // not supported + exportPrivateRawKey: null, + importPrivatePkcs8Key: (keyData, keyImportParams) => + X25519PrivateKey.importPkcs8Key(keyData), + exportPrivatePkcs8Key: (key) => key.exportPkcs8Key(), + importPrivateJsonWebKey: (jsonWebKeyData, keyImportParams) => + X25519PrivateKey.importJsonWebKey(jsonWebKeyData), + exportPrivateJsonWebKey: (key) => key.exportJsonWebKey(), + importPublicRawKey: (keyData, keyImportParams) => + X25519PublicKey.importRawKey(keyData), + exportPublicRawKey: (key) => key.exportRawKey(), + importPublicSpkiKey: (keyData, keyImportParams) => + X25519PublicKey.importSpkiKey(keyData), + exportPublicSpkiKey: (key) => key.exportSpkiKey(), + importPublicJsonWebKey: (jsonWebKeyData, keyImportParams) => + X25519PublicKey.importJsonWebKey(jsonWebKeyData), + exportPublicJsonWebKey: (key) => key.exportJsonWebKey(), + generateKeyPair: (generateKeyPairParams) async { + // Use public / private keys from two different pairs, as if they had been + // exchanged. + final a = await X25519PrivateKey.generateKey(); + final b = await X25519PrivateKey.generateKey(); + return ( + privateKey: a.privateKey, + publicKey: b.publicKey, + ); + }, + deriveBits: (keys, length, deriveParams) => keys.privateKey.deriveBits( + length, + keys.publicKey, + ), + testData: _testData, +); + +void main() async { + log('generate X25519 test case'); + await runner.generate( + generateKeyParams: {}, + importKeyParams: {}, + maxDeriveLength: 256, + ); + log('--------------------'); + + await runner.tests().runTests(); +} + +// Allow single quotes for hardcoded testData written as JSON: +// ignore_for_file: prefer_single_quotes +final _testData = [ + { + "name": "generated on boringssl/mac at 2025-10-06T12:24:27", + "privatePkcs8KeyData": + "MC4CAQAwBQYDK2VuBCIEIOeCpzucGHA4bexZ1GZ7t4uAgIn21fihfYvrTXWdEd2A", + "privateJsonWebKeyData": { + "kty": "OKP", + "crv": "X25519", + "x": "V2VUjKPgzM3fjJDZ8FLsfcgYWMDEYexMCil_vQ8oJ2Q", + "d": "54KnO5wYcDht7FnUZnu3i4CAifbV-KF9i-tNdZ0R3YA" + }, + "publicRawKeyData": "Oun0StbIlmZsImGBVF8PrTzWyFhdqP5cKfapLeHIRRI=", + "publicSpkiKeyData": + "MCowBQYDK2VuAyEAOun0StbIlmZsImGBVF8PrTzWyFhdqP5cKfapLeHIRRI=", + "publicJsonWebKeyData": { + "kty": "OKP", + "crv": "X25519", + "x": "Oun0StbIlmZsImGBVF8PrTzWyFhdqP5cKfapLeHIRRI" + }, + "derivedBits": "j671vmVMMI6R+ud2QxtiKjIwoQbPd66wskZklxA=", + "derivedLength": 228, + "importKeyParams": {}, + "deriveParams": {} + } +]; diff --git a/lib/src/third_party/boringssl/ffigen.yaml b/lib/src/third_party/boringssl/ffigen.yaml index e015bb82..1adf0109 100644 --- a/lib/src/third_party/boringssl/ffigen.yaml +++ b/lib/src/third_party/boringssl/ffigen.yaml @@ -32,7 +32,9 @@ macros: - AES_BLOCK_SIZE - EC_PKEY_NO_PUBKEY - EVP_PKEY_EC + - EVP_PKEY_ED25519 - EVP_PKEY_RSA + - EVP_PKEY_X25519 - HKDF_R_OUTPUT_TOO_LARGE - NID_secp384r1 - NID_secp521r1 @@ -122,10 +124,12 @@ functions: - EVP_CipherUpdate - EVP_DigestFinal - EVP_DigestInit + - EVP_DigestSign - EVP_DigestSignFinal - EVP_DigestSignInit - EVP_DigestSignUpdate - EVP_DigestUpdate + - EVP_DigestVerify - EVP_DigestVerifyFinal - EVP_DigestVerifyInit - EVP_DigestVerifyUpdate @@ -138,6 +142,7 @@ functions: - EVP_parse_public_key - EVP_PKEY_CTX_free - EVP_PKEY_CTX_new + - EVP_PKEY_CTX_new_id - EVP_PKEY_CTX_set0_rsa_oaep_label - EVP_PKEY_CTX_set_rsa_mgf1_md - EVP_PKEY_CTX_set_rsa_oaep_md @@ -145,13 +150,22 @@ functions: - EVP_PKEY_CTX_set_rsa_pss_saltlen - EVP_PKEY_decrypt - EVP_PKEY_decrypt_init + - EVP_PKEY_derive + - EVP_PKEY_derive_init + - EVP_PKEY_derive_set_peer - EVP_PKEY_encrypt - EVP_PKEY_encrypt_init - EVP_PKEY_free + - EVP_PKEY_get_raw_private_key + - EVP_PKEY_get_raw_public_key - EVP_PKEY_get1_EC_KEY - EVP_PKEY_get1_RSA - EVP_PKEY_id + - EVP_PKEY_keygen + - EVP_PKEY_keygen_init - EVP_PKEY_new + - EVP_PKEY_new_raw_private_key + - EVP_PKEY_new_raw_public_key - EVP_PKEY_set1_EC_KEY - EVP_PKEY_set1_RSA - EVP_sha1 diff --git a/lib/src/third_party/boringssl/generated_bindings.dart b/lib/src/third_party/boringssl/generated_bindings.dart index 52f40e00..c3299893 100644 --- a/lib/src/third_party/boringssl/generated_bindings.dart +++ b/lib/src/third_party/boringssl/generated_bindings.dart @@ -1533,6 +1533,41 @@ class BoringSsl { late final _EVP_DigestInit = _EVP_DigestInitPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer)>(); + /// EVP_DigestSign signs |data_len| bytes from |data| using |ctx|. If |out_sig| + /// is NULL then |*out_sig_len| is set to the maximum number of output + /// bytes. Otherwise, on entry, |*out_sig_len| must contain the length of the + /// |out_sig| buffer. If the call is successful, the signature is written to + /// |out_sig| and |*out_sig_len| is set to its length. + /// + /// It returns one on success and zero on error. + int EVP_DigestSign( + ffi.Pointer ctx, + ffi.Pointer out_sig, + ffi.Pointer out_sig_len, + ffi.Pointer data, + int data_len, + ) { + return _EVP_DigestSign( + ctx, + out_sig, + out_sig_len, + data, + data_len, + ); + } + + late final _EVP_DigestSignPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('EVP_DigestSign'); + late final _EVP_DigestSign = _EVP_DigestSignPtr.asFunction< + int Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer, ffi.Pointer, int)>(); + /// EVP_DigestSignFinal signs the data that has been included by one or more /// calls to |EVP_DigestSignUpdate|. If |out_sig| is NULL then |*out_sig_len| is /// set to the maximum number of output bytes. Otherwise, on entry, @@ -1657,6 +1692,32 @@ class BoringSsl { late final _EVP_DigestUpdate = _EVP_DigestUpdatePtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, int)>(); + /// EVP_DigestVerify verifies that |sig_len| bytes from |sig| are a valid + /// signature for |data|. It returns one on success or zero on error. + int EVP_DigestVerify( + ffi.Pointer ctx, + ffi.Pointer sig, + int sig_len, + ffi.Pointer data, + int len, + ) { + return _EVP_DigestVerify( + ctx, + sig, + sig_len, + data, + len, + ); + } + + late final _EVP_DigestVerifyPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, ffi.Pointer, + ffi.Size, ffi.Pointer, ffi.Size)>>('EVP_DigestVerify'); + late final _EVP_DigestVerify = _EVP_DigestVerifyPtr.asFunction< + int Function(ffi.Pointer, ffi.Pointer, int, + ffi.Pointer, int)>(); + /// EVP_DigestVerifyFinal verifies that |sig_len| bytes of |sig| are a valid /// signature for the data that has been included by one or more calls to /// |EVP_DigestVerifyUpdate|. It returns one on success and zero otherwise. @@ -1848,6 +1909,27 @@ class BoringSsl { ffi.Pointer Function( ffi.Pointer, ffi.Pointer)>(); + /// EVP_PKEY_CTX_new_id allocates a fresh |EVP_PKEY_CTX| for a key of type |id| + /// (e.g. |EVP_PKEY_HMAC|). This can be used for key generation where + /// |EVP_PKEY_CTX_new| can't be used because there isn't an |EVP_PKEY| to pass + /// it. It returns the context or NULL on error. + ffi.Pointer EVP_PKEY_CTX_new_id( + int id, + ffi.Pointer e, + ) { + return _EVP_PKEY_CTX_new_id( + id, + e, + ); + } + + late final _EVP_PKEY_CTX_new_idPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Int, ffi.Pointer)>>('EVP_PKEY_CTX_new_id'); + late final _EVP_PKEY_CTX_new_id = _EVP_PKEY_CTX_new_idPtr.asFunction< + ffi.Pointer Function(int, ffi.Pointer)>(); + /// EVP_PKEY_CTX_set0_rsa_oaep_label sets |label_len| bytes from |label| as the /// label used in OAEP. DANGER: On success, this call takes ownership of |label| /// and will call |OPENSSL_free| on it when |ctx| is destroyed. @@ -2026,6 +2108,77 @@ class BoringSsl { late final _EVP_PKEY_decrypt_init = _EVP_PKEY_decrypt_initPtr.asFunction< int Function(ffi.Pointer)>(); + /// EVP_PKEY_derive derives a shared key from |ctx|. If |key| is non-NULL then, + /// on entry, |out_key_len| must contain the amount of space at |key|. If + /// sufficient then the shared key will be written to |key| and |*out_key_len| + /// will be set to the length. If |key| is NULL then |out_key_len| will be set to + /// the maximum length. + /// + /// WARNING: Setting |out| to NULL only gives the maximum size of the key. The + /// actual key may be smaller. + /// + /// It returns one on success and zero on error. + int EVP_PKEY_derive( + ffi.Pointer ctx, + ffi.Pointer key, + ffi.Pointer out_key_len, + ) { + return _EVP_PKEY_derive( + ctx, + key, + out_key_len, + ); + } + + late final _EVP_PKEY_derivePtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer)>>('EVP_PKEY_derive'); + late final _EVP_PKEY_derive = _EVP_PKEY_derivePtr.asFunction< + int Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer)>(); + + /// EVP_PKEY_derive_init initialises an |EVP_PKEY_CTX| for a key derivation + /// operation. It should be called before |EVP_PKEY_derive_set_peer| and + /// |EVP_PKEY_derive|. + /// + /// It returns one on success or zero on error. + int EVP_PKEY_derive_init( + ffi.Pointer ctx, + ) { + return _EVP_PKEY_derive_init( + ctx, + ); + } + + late final _EVP_PKEY_derive_initPtr = + _lookup)>>( + 'EVP_PKEY_derive_init'); + late final _EVP_PKEY_derive_init = _EVP_PKEY_derive_initPtr.asFunction< + int Function(ffi.Pointer)>(); + + /// EVP_PKEY_derive_set_peer sets the peer's key to be used for key derivation + /// by |ctx| to |peer|. It should be called after |EVP_PKEY_derive_init|. (For + /// example, this is used to set the peer's key in (EC)DH.) It returns one on + /// success and zero on error. + int EVP_PKEY_derive_set_peer( + ffi.Pointer ctx, + ffi.Pointer peer, + ) { + return _EVP_PKEY_derive_set_peer( + ctx, + peer, + ); + } + + late final _EVP_PKEY_derive_set_peerPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, + ffi.Pointer)>>('EVP_PKEY_derive_set_peer'); + late final _EVP_PKEY_derive_set_peer = + _EVP_PKEY_derive_set_peerPtr.asFunction< + int Function(ffi.Pointer, ffi.Pointer)>(); + /// EVP_PKEY_encrypt encrypts |in_len| bytes from |in|. If |out| is NULL, the /// maximum size of the ciphertext is written to |out_len|. Otherwise, |*out_len| /// must contain the number of bytes of space available at |out|. If sufficient, @@ -2127,6 +2280,62 @@ class BoringSsl { late final _EVP_PKEY_get1_RSA = _EVP_PKEY_get1_RSAPtr.asFunction< ffi.Pointer Function(ffi.Pointer)>(); + /// EVP_PKEY_get_raw_private_key outputs the private key for |pkey| in raw form. + /// If |out| is NULL, it sets |*out_len| to the size of the raw private key. + /// Otherwise, it writes at most |*out_len| bytes to |out| and sets |*out_len| to + /// the number of bytes written. + /// + /// It returns one on success and zero if |pkey| has no private key, the key + /// type does not support a raw format, or the buffer is too small. + int EVP_PKEY_get_raw_private_key( + ffi.Pointer pkey, + ffi.Pointer out, + ffi.Pointer out_len, + ) { + return _EVP_PKEY_get_raw_private_key( + pkey, + out, + out_len, + ); + } + + late final _EVP_PKEY_get_raw_private_keyPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer)>>('EVP_PKEY_get_raw_private_key'); + late final _EVP_PKEY_get_raw_private_key = + _EVP_PKEY_get_raw_private_keyPtr.asFunction< + int Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer)>(); + + /// EVP_PKEY_get_raw_public_key outputs the public key for |pkey| in raw form. + /// If |out| is NULL, it sets |*out_len| to the size of the raw public key. + /// Otherwise, it writes at most |*out_len| bytes to |out| and sets |*out_len| to + /// the number of bytes written. + /// + /// It returns one on success and zero if |pkey| has no public key, the key + /// type does not support a raw format, or the buffer is too small. + int EVP_PKEY_get_raw_public_key( + ffi.Pointer pkey, + ffi.Pointer out, + ffi.Pointer out_len, + ) { + return _EVP_PKEY_get_raw_public_key( + pkey, + out, + out_len, + ); + } + + late final _EVP_PKEY_get_raw_public_keyPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer)>>('EVP_PKEY_get_raw_public_key'); + late final _EVP_PKEY_get_raw_public_key = + _EVP_PKEY_get_raw_public_keyPtr.asFunction< + int Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer)>(); + /// EVP_PKEY_id returns the type of |pkey|, which is one of the |EVP_PKEY_*| /// values. int EVP_PKEY_id( @@ -2143,6 +2352,46 @@ class BoringSsl { late final _EVP_PKEY_id = _EVP_PKEY_idPtr.asFunction)>(); + /// EVP_PKEY_keygen performs a key generation operation using the values from + /// |ctx|. If |*out_pkey| is non-NULL, it overwrites |*out_pkey| with the + /// resulting key. Otherwise, it sets |*out_pkey| to a newly-allocated |EVP_PKEY| + /// containing the result. It returns one on success or zero on error. + int EVP_PKEY_keygen( + ffi.Pointer ctx, + ffi.Pointer> out_pkey, + ) { + return _EVP_PKEY_keygen( + ctx, + out_pkey, + ); + } + + late final _EVP_PKEY_keygenPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, + ffi.Pointer>)>>('EVP_PKEY_keygen'); + late final _EVP_PKEY_keygen = _EVP_PKEY_keygenPtr.asFunction< + int Function( + ffi.Pointer, ffi.Pointer>)>(); + + /// EVP_PKEY_keygen_init initialises an |EVP_PKEY_CTX| for a key generation + /// operation. It should be called before |EVP_PKEY_keygen|. + /// + /// It returns one on success or zero on error. + int EVP_PKEY_keygen_init( + ffi.Pointer ctx, + ) { + return _EVP_PKEY_keygen_init( + ctx, + ); + } + + late final _EVP_PKEY_keygen_initPtr = + _lookup)>>( + 'EVP_PKEY_keygen_init'); + late final _EVP_PKEY_keygen_init = _EVP_PKEY_keygen_initPtr.asFunction< + int Function(ffi.Pointer)>(); + /// EVP_PKEY_new creates a new, empty public-key object and returns it or NULL /// on allocation failure. ffi.Pointer EVP_PKEY_new() { @@ -2155,6 +2404,64 @@ class BoringSsl { late final _EVP_PKEY_new = _EVP_PKEY_newPtr.asFunction Function()>(); + /// EVP_PKEY_new_raw_private_key returns a newly allocated |EVP_PKEY| wrapping a + /// private key of the specified type. It returns one on success and zero on + /// error. + ffi.Pointer EVP_PKEY_new_raw_private_key( + int type, + ffi.Pointer unused, + ffi.Pointer in1, + int len, + ) { + return _EVP_PKEY_new_raw_private_key( + type, + unused, + in1, + len, + ); + } + + late final _EVP_PKEY_new_raw_private_keyPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Int, + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('EVP_PKEY_new_raw_private_key'); + late final _EVP_PKEY_new_raw_private_key = + _EVP_PKEY_new_raw_private_keyPtr.asFunction< + ffi.Pointer Function( + int, ffi.Pointer, ffi.Pointer, int)>(); + + /// EVP_PKEY_new_raw_public_key returns a newly allocated |EVP_PKEY| wrapping a + /// public key of the specified type. It returns one on success and zero on + /// error. + ffi.Pointer EVP_PKEY_new_raw_public_key( + int type, + ffi.Pointer unused, + ffi.Pointer in1, + int len, + ) { + return _EVP_PKEY_new_raw_public_key( + type, + unused, + in1, + len, + ); + } + + late final _EVP_PKEY_new_raw_public_keyPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Int, + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('EVP_PKEY_new_raw_public_key'); + late final _EVP_PKEY_new_raw_public_key = + _EVP_PKEY_new_raw_public_keyPtr.asFunction< + ffi.Pointer Function( + int, ffi.Pointer, ffi.Pointer, int)>(); + int EVP_PKEY_set1_EC_KEY( ffi.Pointer pkey, ffi.Pointer key, @@ -3029,8 +3336,12 @@ typedef EVP_PKEY_CTX = evp_pkey_ctx_st; const int EVP_PKEY_EC = 408; +const int EVP_PKEY_ED25519 = 949; + const int EVP_PKEY_RSA = 6; +const int EVP_PKEY_X25519 = 948; + const int HKDF_R_OUTPUT_TOO_LARGE = 100; typedef HMAC_CTX = hmac_ctx_st; diff --git a/lib/src/webcrypto/webcrypto.dart b/lib/src/webcrypto/webcrypto.dart index a6e240e2..45fd5eee 100644 --- a/lib/src/webcrypto/webcrypto.dart +++ b/lib/src/webcrypto/webcrypto.dart @@ -37,6 +37,7 @@ part 'webcrypto.aesgcm.dart'; part 'webcrypto.digest.dart'; part 'webcrypto.ecdh.dart'; part 'webcrypto.ecdsa.dart'; +part 'webcrypto.ed25519.dart'; part 'webcrypto.hkdf.dart'; part 'webcrypto.hmac.dart'; part 'webcrypto.pbkdf2.dart'; @@ -44,3 +45,4 @@ part 'webcrypto.random.dart'; part 'webcrypto.rsaoaep.dart'; part 'webcrypto.rsapss.dart'; part 'webcrypto.rsassapkcs1v15.dart'; +part 'webcrypto.x25519.dart'; diff --git a/lib/src/webcrypto/webcrypto.ed25519.dart b/lib/src/webcrypto/webcrypto.ed25519.dart new file mode 100644 index 00000000..64382aa5 --- /dev/null +++ b/lib/src/webcrypto/webcrypto.ed25519.dart @@ -0,0 +1,437 @@ +part of 'webcrypto.dart'; + +/// Ed25519 private key for signing messages. +/// +/// An [Ed25519PrivateKey] instance holds a private key for computing +/// signatures using the EdDSA scheme as specified in [RFC 8032][1]. +/// +/// An [Ed25519PrivateKey] can be imported from: +/// * PKCS8 Key using [Ed25519PrivateKey.importPkcs8Key], and, +/// * JSON Web Key using [Ed25519PrivateKey.importJsonWebKey]. +/// +/// A public-private [KeyPair] consisting of a [Ed25519PublicKey] and a +/// [Ed25519PrivateKey] can be generated using [Ed25519PrivateKey.generateKey]. +/// +/// {@template Ed25519-Example:generate-sign-verify} +/// **Example** +/// ```dart +/// import 'dart:convert' show utf8; +/// import 'package:webcrypto/webcrypto.dart'; +/// +/// // Generate a key-pair. +/// final keyPair = await Ed25519PrivateKey.generateKey(); +/// +/// // Using privateKey Bob can sign a message for Alice. +/// final message = 'Hi Alice'; +/// final signature = await keyPair.privateKey.signBytes(utf8.encode(message)); +/// +/// // Given publicKey and signature Alice can verify the message from Bob. +/// final isValid = await keypair.publicKey.verifyBytes( +/// signature, +/// utf8.encode(message), +/// ); +/// if (isValid) { +/// print('Authentic message from Bob: $message'); +/// } +/// ``` +/// {@endtemplate} +/// +/// [1]: https://datatracker.ietf.org/doc/html/rfc8032 +final class Ed25519PrivateKey { + final Ed25519PrivateKeyImpl _impl; + + Ed25519PrivateKey._(this._impl); + + /// Import [Ed25519PrivateKey] in the [PKCS #8][1] format. + /// + /// Creates an [Ed25519PrivateKey] from [keyData] given as the DER encodeding + /// _PrivateKeyInfo structure_ specified in [RFC 5208][1]. + /// + /// **Example** + /// ```dart + /// import 'package:pem/pem.dart'; + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// // Read key data from a PEM encoded block. This will remove the + /// // the padding, decode base64 and return the encoded bytes. + /// List keyData = PemCodec(PemLabel.privateKey).decode(''' + /// -----BEGIN PRIVATE KEY----- + /// MC4CAQAwBQYDK2VuBCI..... + /// -----END PRIVATE KEY----- + /// '''); + /// + /// + /// Future main() async { + /// // Import the Private Key from a Binary PEM decoded data. + /// final privateKey = await Ed25519PrivateKey.importPkcs8Key(keyData); + /// + /// // Export the private key (print it in same format as it was given). + /// final exportedPkcs8Key = await privateKey.exportPkcs8Key(); + /// print(PemCodec(PemLabel.privateKey).encode(exportedPkcs8Key)); + /// } + /// ``` + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc5208 + static Future importPkcs8Key(List keyData) async { + final impl = await webCryptImpl.ed25519PrivateKey.importPkcs8Key(keyData); + return Ed25519PrivateKey._(impl); + } + + /// Import Ed25519 private key in [JSON Web Key][1] format. + /// + /// {@macro importJsonWebKey:jwk} + /// + /// JSON Web Keys imported using [Ed25519PrivateKey.importJsonWebKey] must + /// have the following parameters as described in [RFC 8037][2]: + /// * `"kty"`: The key type must be `"OKP"`. + /// * `"crv"`: The curve parameter must be `"Ed25519"`. + /// * `"alg"`: The alg parameter must be `"Ed25519"` or `"EdDSA"`, if present. + /// * `"x"`: The x parameter must be present and contain the public key + /// encoded as a [base64Url] encoded string. + /// * `"d"`: The d parameter must be present for private keys and contain the + /// private key encoded as a [base64Url] encoded string. + /// + /// **Example** + /// ```dart + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// // JSON Web Key as map representing the decoded JSON. + /// final jwk = { + /// 'kty': 'OKP', + /// 'crv': 'Ed25519', + /// 'x': 'coeKtJr7mBuIBzGjR_T4OFfuU3Sn85-frLUvxzg5320', + /// 'd': '0iqe8CdaOI0uP_UG7wzEQanilG-UWWw4tuSeMKNXnKA', + /// }; + /// + /// Future main() async { + /// // Import secret key from decoded JSON. + /// final jsonWebKey = await Ed25519PrivateKey.importJsonWebKey(jwk); + /// + /// // Export the key (print it in same format as it was given). + /// final exportedJsonWebKey = await jsonWebKey.exportJsonWebKey(); + /// print(exportedJsonWebKey); + /// } + /// ``` + /// + /// [1]: https://tools.ietf.org/html/rfc7517 + /// [2]: https://datatracker.ietf.org/doc/html/rfc8037#section-2 + static Future importJsonWebKey( + Map jwk, + ) async { + final impl = await webCryptImpl.ed25519PrivateKey.importJsonWebKey(jwk); + return Ed25519PrivateKey._(impl); + } + + /// Generate a new [Ed25519PrivateKey] and [Ed25519PublicKey] pair. + /// + /// **Example** + /// ```dart + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// Future main() async { + /// // Generate a new Ed25519 key pair. + /// final keyPair = await Ed25519PrivateKey.generateKey(); + /// + /// // Export the private key. + /// final exportedPrivateKey = await keyPair.privateKey.exportJsonWebKey(); + /// print(exportedPrivateKey); + /// + /// // Export the public key. + /// final exportedPublicKey = await keyPair.publicKey.exportJsonWebKey(); + /// print(exportedPublicKey); + /// } + /// ``` + static Future> + generateKey() async { + final (privateKeyImpl, publicKeyImpl) = + await webCryptImpl.ed25519PrivateKey.generateKey(); + + final privateKey = Ed25519PrivateKey._(privateKeyImpl); + final publicKey = Ed25519PublicKey._(publicKeyImpl); + + return (privateKey: privateKey, publicKey: publicKey); + } + + /// Sign [data] with this [Ed25519PrivateKey]. + /// + /// Returns a signature as a list of raw bytes.2]. + /// + /// **Example** + /// ```dart + /// import 'dart:convert' show utf8, base64; + /// import 'package:webcrypto/webcrypto.dart'; + /// import 'package:pem/pem.dart'; + /// + /// // Read prviate key data from PEM encoded block. This will remove the + /// // '----BEGIN...' padding, decode base64 and return encoded bytes. + /// List keyData = PemCodec(PemLabel.privateKey).decode(""" + /// -----BEGIN PRIVATE KEY----- + /// MC4CAQAwBQYDK2VuBCI..... + /// -----END PRIVATE KEY----- + /// """); + /// + /// // Import private key from binary PEM decoded data. + /// final privatKey = await Ed25519PrivateKey.importPkcs8Key(keyData); + /// + /// // Create a signature for UTF-8 encoded message + /// final message = 'hello world'; + /// final signature = await privateKey.signBytes(utf8.encode(message)); + /// + /// print('signature: ${base64.encode(signature)}'); + /// ``` + Future signBytes(List data) => _impl.signBytes(data); + + /// Export the [Ed25519PrivateKey] as a [PKCS #8][1] key. + /// + /// Returns the DER encoding of the _PrivateKeyInfo_ structure specified in + /// [RFC 5208][1] as a list of bytes. + /// + /// **Example** + /// ```dart + /// import 'package:pem/pem.dart'; + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// Future main() async { + /// // Generate a key-pair + /// final kp = await Ed25519PrivateKey.generateKey(); + /// + /// // Export the private key. + /// final exportedPkcs8Key = await kp.privateKey.exportPkcs8Key(); + /// + /// // Private keys are often encoded as PEM. + /// // This encodes the key in base64 and wraps it with: + /// // '-----BEGIN PRIVATE KEY----'... + /// print(PemCodec(PemLabel.privateKey).encode(exportedPkcs8Key)); + /// } + /// ``` + /// [1]: https://datatracker.ietf.org/doc/html/rfc5208 + Future exportPkcs8Key() async => _impl.exportPkcs8Key(); + + /// Export the [Ed25519PrivateKey] as a [JSON Web Key][1]. + /// + /// {@macro exportJsonWebKey:returns} + /// + /// **Example** + /// ```dart + /// import 'dart:convert'; + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// Future main() async { + /// // Alice generates a key-pair + /// final kpA = await Ed25519PrivateKey.generateKey(); + /// + /// // Export the private key as a JSON Web Key. + /// final exportedPrivateKey = await kpA.privateKey.exportJsonWebKey(); + /// + /// // The Map returned by `exportJsonWebKey()` can be converted to JSON + /// // with `jsonEncode` from `dart:convert`. + /// print(jsonEncode(exportedPrivateKey)); + /// } + /// ``` + /// [1]: https://www.rfc-editor.org/rfc/rfc7518.html + Future> exportJsonWebKey() => _impl.exportJsonWebKey(); +} + +/// Ed25519 public key for verifying signatures. +/// +/// An [Ed25519PublicKey] instance holds a public Ed25519 key for verification +/// of signatures using the EdDSA scheme as specified in [RFC 8032][1]. +/// +/// An [Ed25519PublicKey] can be imported from: +/// * [SPKI][2] format using [Ed25519PublicKey.importSpkiKey], and, +/// * [JWK][3] format using [Ed25519PublicKey.importJsonWebKey]. +/// +/// A public-private [KeyPair] consisting of a [Ed25519PublicKey] and a +/// [Ed25519PrivateKey] can be generated using [Ed25519PrivateKey.generateKey]. +/// +/// {@macro Ed25519-Example:generate-sign-verify} +/// +/// [1]: https://datatracker.ietf.org/doc/html/rfc8032 +/// [2]: https://tools.ietf.org/html/rfc5280 +/// [3]: https://tools.ietf.org/html/rfc7517 +final class Ed25519PublicKey { + final Ed25519PublicKeyImpl _impl; + + Ed25519PublicKey._(this._impl); + + static Future importRawKey(List keyData) async { + final impl = await webCryptImpl.ed25519PublicKey.importRawKey(keyData); + return Ed25519PublicKey._(impl); + } + + /// Import Ed25519 public key in SPKI format. + /// + /// Creates an [Ed25519PublicKey] from [keyData] given as the DER + /// encoding of the _SubjectPublicKeyInfo structure_ specified in + /// [RFC 5280][1]. + /// + /// Throws [FormatException] if [keyData] is invalid. + /// + /// **Example** + /// ```dart + /// import 'package:webcrypto/webcrypto.dart'; + /// import 'package:pem/pem.dart'; + /// + /// // Read key data from PEM encoded block. This will remove the + /// // '----BEGIN...' padding, decode base64 and return encoded bytes. + /// List keyData = PemCodec(PemLabel.publicKey).decode(""" + /// -----BEGIN PUBLIC KEY----- + /// MCowBQYDK2VwAyEA... + /// -----END PUBLIC KEY----- + /// """); + /// + /// // Import public key from binary PEM decoded data. + /// final publicKey = await Ed25519PublicKey.importSpkiKey(keyData); + /// + /// // Export the key again (print it in same format as it was given). + /// List rawKeyData = await publicKey.exportSpkiKey(); + /// print(PemCodec(PemLabel.publicKey).encode(rawKeyData)); + /// ``` + /// + /// [1]: https://tools.ietf.org/html/rfc5280 + static Future importSpkiKey(List keyData) async { + final impl = await webCryptImpl.ed25519PublicKey.importSpkiKey(keyData); + return Ed25519PublicKey._(impl); + } + + /// Import Ed25519 public key in [JSON Web Key][1] format. + /// + /// {@macro importJsonWebKey:jwk} + /// + /// JSON Web Keys imported using [Ed25519PublicKey.importJsonWebKey] must + /// have the following parameters as described in [RFC 8037][2]: + /// * `"kty"`: The key type must be `"OKP"`. + /// * `"crv"`: The curve parameter must be `"Ed25519"`. + /// * `"x"`: The x parameter must be present and contain the public key + /// encoded as a [base64Url] encoded string. + /// + /// **Example** + /// ```dart + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// // JSON Web Key as map representing the decoded JSON. + /// final jwk = { + /// 'kty': 'OKP', + /// 'crv': 'Ed25519', + /// 'x': 'coeKtJr7mBuIBzGjR_T4OFfuU3Sn85-frLUvxzg5320', + /// }; + /// + /// Future main() async { + /// // Import the public key from decoded JSON. + /// final jsonWebKey = await Ed25519PublicKey.importJsonWebKey(jwk); + /// + /// // Export the key (print it in same format as it was given). + /// final exportedJsonWebKey = await jsonWebKey.exportJsonWebKey(); + /// print(exportedJsonWebKey); + /// } + /// ``` + /// + /// [1]: https://tools.ietf.org/html/rfc7517 + /// [2]: https://datatracker.ietf.org/doc/html/rfc8037#section-2 + static Future importJsonWebKey( + Map jwk, + ) async { + final impl = await webCryptImpl.ed25519PublicKey.importJsonWebKey(jwk); + return Ed25519PublicKey._(impl); + } + + /// Verify [signature] of [data] using this Ed25519 public key. + /// + /// Returns `true` if the signature was made the private key matching this + /// public key. + /// + /// **Example** + /// ```dart + /// import 'dart:convert' show utf8; + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// // Generate a key-pair. + /// final keyPair = await Ed25519PrivateKey.generateKey(); + /// + /// // Using privateKey Bob can sign a message for Alice. + /// final message = 'Hi Alice'; + /// final signature = await keyPair.privateKey.signBytes( + /// utf8.encode(message), + /// ); + /// + /// // Given publicKey and signature Alice can verify the message from Bob. + /// final isValid = await keypair.publicKey.verifyBytes( + /// signature, + /// utf8.encode(message), + /// ); + /// if (isValid) { + /// print('Authentic message from Bob: $message'); + /// } + /// ``` + Future verifyBytes(List signature, List data) => + _impl.verifyBytes(signature, data); + + /// Export Ed25519 public key as a raw list of bytes. + /// + /// **Example** + /// ```dart + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// // Generate a key-pair. + /// final keyPair = await Ed25519PrivateKey.generateKey(); + /// + /// // Export the public key as raw bytes. + /// final rawPublicKey = await keyPair.publicKey.exportRawKey(); + Future exportRawKey() => _impl.exportRawKey(); + + /// Export Ed25519 public key in SPKI format. + /// + /// Returns the DER encoding of the _SubjectPublicKeyInfo structure_ specified + /// in [RFC 5280][1] as a list of bytes. + /// + /// **Example** + /// ```dart + /// import 'package:webcrypto/webcrypto.dart'; + /// import 'package:pem/pem.dart'; + /// + /// // Generate a key-pair. + /// final keyPair = await Ed25519PrivateKey.generateKey(); + /// + /// // Export the public key. + /// final spkiPublicKey = await keyPair.publicKey.exportSpkiKey(); + /// + /// // Public keys are often encoded as PEM. + /// // This encode the key in base64 and wraps it with: + /// // '-----BEGIN PUBLIC KEY-----'... + /// print(PemCodec(PemLabel.publicKey).encode(spkiPublicKey)); + /// ``` + /// + /// [1]: https://tools.ietf.org/html/rfc5280 + Future exportSpkiKey() => _impl.exportSpkiKey(); + + /// Export the [Ed25519PublicKey] as a [JSON Web Key][1]. + /// + /// {@macro exportJsonWebKey:returns} + /// + /// **Example** + /// ```dart + /// import 'dart:convert'; + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// // Public JSON Web Key data. + /// final jwk = { + /// 'kty': 'OKP', + /// 'crv': 'Ed25519', + /// 'x': 'coeKtJr7mBuIBzGjR_T4OFfuU3Sn85-frLUvxzg5320', + /// }; + /// + /// Future main() async { + /// // Import Alice's public key + /// final pkA = await Ed25519PublicKey.importJsonWebKey(jwk); + /// + /// // Export the public key as a JSON Web Key. + /// final exportedPublicKey = await pkA.exportJsonWebKey(); + /// + /// // The Map returned by `exportJsonWebKey()` can be converted to JSON + /// // with `jsonEncode` from `dart:convert`. + /// print(jsonEncode(exportedPublicKey)); + /// } + /// ``` + /// [1]: https://www.rfc-editor.org/rfc/rfc7518.html + Future> exportJsonWebKey() => _impl.exportJsonWebKey(); +} diff --git a/lib/src/webcrypto/webcrypto.x25519.dart b/lib/src/webcrypto/webcrypto.x25519.dart new file mode 100644 index 00000000..c73f48d1 --- /dev/null +++ b/lib/src/webcrypto/webcrypto.x25519.dart @@ -0,0 +1,380 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +part of 'webcrypto.dart'; + +/// X25519 private key for deriving a shared secret. +/// +/// Elliptic Curve Diffie-Hellman (ECDH) is a key agreement protocol that allows +/// two parties to establish a shared secret over an insecure channel. +/// An [X25519PrivateKey] holds a private key that can be used to derive a +/// shared secret given the public key from a different key pair. +/// +/// Instances of [X25519PrivateKey] can be imported from: +/// * PKCS8 Key using [X25519PrivateKey.importPkcs8Key], and, +/// * JSON Web Key using [X25519PrivateKey.importJsonWebKey]. +/// +/// A key pair can be generated using [X25519PrivateKey.generateKey]. +/// +/// {@template X25519PrivateKey:example} +/// **Example** +/// ```dart +/// import 'dart:convert'; +/// import 'package:webcrypto/webcrypto.dart'; +/// +/// Future main() async { +/// // Alice generates a key-pair +/// final kpA = await X25519PrivateKey.generateKey(); +/// +/// // Bob generates a key-pair +/// final kpB = await X25519PrivateKey.generateKey(); +/// +/// // Alice can make a shared secret using Bob's public key +/// final sharedSecretA = await kpA.privateKey.deriveBits(256, kpB.publicKey); +/// +/// // Bob can make the same shared secret using Alice public key +/// final sharedSecretB = await kpB.privateKey.deriveBits(256, kpA.publicKey); +/// +/// // Alice and Bob should have the same shared secret +/// assert(base64.encode(sharedSecretA) == base64.encode(sharedSecretB)); +/// } +/// ``` +/// {@endtemplate} +final class X25519PrivateKey { + final X25519PrivateKeyImpl _impl; + + X25519PrivateKey._(this._impl); // keep the constructor private. + + /// Import [X25519PrivateKey] in the [PKCS #8][1] format. + /// + /// Creates an [X25519PrivateKey] from [keyData] given as the DER encodeding + /// _PrivateKeyInfo structure_ specified in [RFC 5208][1]. + /// + /// **Example** + /// ```dart + /// import 'package:pem/pem.dart'; + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// // Read key data from a PEM encoded block. This will remove the + /// // the padding, decode base64 and return the encoded bytes. + /// List keyData = PemCodec(PemLabel.privateKey).decode(''' + /// -----BEGIN PRIVATE KEY----- + /// MC4CAQAwBQYDK2VuBCI..... + /// -----END PRIVATE KEY----- + /// '''); + /// + /// + /// Future main() async { + /// // Import the Private Key from a Binary PEM decoded data. + /// final privateKey = await X25519PrivateKey.importPkcs8Key(keyData); + /// + /// // Export the private key (print it in same format as it was given). + /// final exportedPkcs8Key = await privateKey.exportPkcs8Key(); + /// print(PemCodec(PemLabel.privateKey).encode(exportedPkcs8Key)); + /// } + /// ``` + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc5208 + static Future importPkcs8Key(List keyData) async { + final impl = await webCryptImpl.x25519PrivateKey.importPkcs8Key(keyData); + return X25519PrivateKey._(impl); + } + + /// Import X25519 private key in [JSON Web Key][1] format. + /// + /// {@macro importJsonWebKey:jwk} + /// + /// JSON Web Keys imported using [X25519PrivateKey.importJsonWebKey] must + /// have the following parameters as described in [RFC 8037][2]: + /// * `"kty"`: The key type must be `"OKP"`. + /// * `"crv"`: The curve parameter must be `"X25519"`. + /// * `"x"`: The x parameter must be present and contain the public key + /// encoded as a [base64Url] encoded string. + /// * `"d"`: The d parameter must be present for private keys and contain the + /// private key encoded as a [base64Url] encoded string. + /// + /// **Example** + /// ```dart + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// // JSON Web Key as map representing the decoded JSON. + /// final jwk = { + /// 'kty': 'OKP', + /// 'crv': 'X25519', + /// 'x': 'coeKtJr7mBuIBzGjR_T4OFfuU3Sn85-frLUvxzg5320', + /// 'd': '0iqe8CdaOI0uP_UG7wzEQanilG-UWWw4tuSeMKNXnKA', + /// }; + /// + /// Future main() async { + /// // Import secret key from decoded JSON. + /// final jsonWebKey = await X25519PrivateKey.importJsonWebKey(jwk); + /// + /// // Export the key (print it in same format as it was given). + /// final exportedJsonWebKey = await jsonWebKey.exportJsonWebKey(); + /// print(exportedJsonWebKey); + /// } + /// ``` + /// + /// [1]: https://tools.ietf.org/html/rfc7517 + /// [2]: https://datatracker.ietf.org/doc/html/rfc8037#section-2 + static Future importJsonWebKey( + Map jwk, + ) async { + final impl = await webCryptImpl.x25519PrivateKey.importJsonWebKey(jwk); + return X25519PrivateKey._(impl); + } + + /// Generate a new [X25519PrivateKey] and [X25519PublicKey] pair. + /// + /// **Example** + /// ```dart + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// Future main() async { + /// // Generate a new X25519 key pair. + /// final keyPair = await X25519PrivateKey.generateKey(); + /// + /// // Export the private key. + /// final exportedPrivateKey = await keyPair.privateKey.exportJsonWebKey(); + /// print(exportedPrivateKey); + /// + /// // Export the public key. + /// final exportedPublicKey = await keyPair.publicKey.exportJsonWebKey(); + /// print(exportedPublicKey); + /// } + /// ``` + /// + static Future> + generateKey() async { + final (privateKeyImpl, publicKeyImpl) = + await webCryptImpl.x25519PrivateKey.generateKey(); + + final privateKey = X25519PrivateKey._(privateKeyImpl); + final publicKey = X25519PublicKey._(publicKeyImpl); + + return (privateKey: privateKey, publicKey: publicKey); + } + + /// Derive a shared secret from two X25519 key pairs using the private key + /// from one pair and the public key from another. + /// + /// The shared secret is identical whether using A's private key and B's + /// public key, or B's private key and A's public key, enabling secure key + /// exchange between the two parties. + /// + /// [length] specifies the length of the derived secret in bits. + /// [publicKey] is [X25519PublicKey] from the other party's X25519 key pair. + /// + /// Returns a [Uint8List] containing the derived shared secret. + /// + /// {@macro X25519PrivateKey:example} + Future deriveBits(int length, X25519PublicKey publicKey) async { + final publicKeyImpl = publicKey._impl; + return _impl.deriveBits(length, publicKeyImpl); + } + + /// Export the [X25519PrivateKey] as a [PKCS #8][1] key. + /// + /// Returns the DER encoding of the _PrivateKeyInfo_ structure specified in + /// [RFC 5208][1] as a list of bytes. + /// + /// **Example** + /// ```dart + /// import 'package:pem/pem.dart'; + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// Future main() async { + /// // Generate a key-pair + /// final kp = await X25519PrivateKey.generateKey(); + /// + /// // Export the private key. + /// final exportedPkcs8Key = await kp.privateKey.exportPkcs8Key(); + /// + /// // Private keys are often encoded as PEM. + /// // This encodes the key in base64 and wraps it with: + /// // '-----BEGIN PRIVATE KEY----'... + /// print(PemCodec(PemLabel.privateKey).encode(exportedPkcs8Key)); + /// } + /// ``` + /// [1]: https://datatracker.ietf.org/doc/html/rfc5208 + Future exportPkcs8Key() => _impl.exportPkcs8Key(); + + /// Export the [X25519PrivateKey] as a [JSON Web Key][1]. + /// + /// {@macro exportJsonWebKey:returns} + /// + /// **Example** + /// ```dart + /// import 'dart:convert'; + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// Future main() async { + /// // Alice generates a key-pair + /// final kpA = await X25519PrivateKey.generateKey(); + /// + /// // Export the private key as a JSON Web Key. + /// final exportedPrivateKey = await kpA.privateKey.exportJsonWebKey(); + /// + /// // The Map returned by `exportJsonWebKey()` can be converted to JSON + /// // with `jsonEncode` from `dart:convert`. + /// print(jsonEncode(exportedPrivateKey)); + /// } + /// ``` + /// [1]: https://www.rfc-editor.org/rfc/rfc7518.html + Future> exportJsonWebKey() => _impl.exportJsonWebKey(); +} + +/// X25519 public key for deriving a shared secret. +/// +/// An [X25519PublicKey] instance holds an X25519 public key for use with the +/// Elliptic Curve Diffie-Hellman (ECDH) key agreement protocol that allows +/// two parties to establish a shared secret over an insecure channel. +/// +/// An [X25519PublicKey] can be imported from: +/// * Raw format using [X25519PublicKey.importRawKey], +/// * [SPKI][1] format using [X25519PublicKey.importSpkiKey], and, +/// * [JWK][2] format using [X25519PublicKey.importJsonWebKey]. +/// +/// A public-private [KeyPair] consisting of a [X25519PublicKey] and +/// [X25519PrivateKey] can be generated using [X25519PrivateKey.generateKey]. +/// +/// {@macro X25519PrivateKey:example} +/// +/// [1]: https://tools.ietf.org/html/rfc5280 +/// [2]: https://tools.ietf.org/html/rfc7517 +final class X25519PublicKey { + final X25519PublicKeyImpl _impl; + + X25519PublicKey._(this._impl); // keep the constructor private. + + static Future importRawKey(List keyData) async { + final impl = await webCryptImpl.x25519PublicKey.importRawKey(keyData); + return X25519PublicKey._(impl); + } + + static Future importSpkiKey(List keyData) async { + final impl = await webCryptImpl.x25519PublicKey.importSpkiKey(keyData); + return X25519PublicKey._(impl); + } + + /// Import X25519 public key in [JSON Web Key][1] format. + /// + /// {@macro importJsonWebKey:jwk} + /// + /// JSON Web Keys imported using [X25519PublicKey.importJsonWebKey] must + /// have the following parameters as described in [RFC 8037][2]: + /// * `"kty"`: The key type must be `"OKP"`. + /// * `"crv"`: The curve parameter must be `"X25519"`. + /// * `"x"`: The x parameter must be present and contain the public key + /// encoded as a [base64Url] encoded string. + /// + /// **Example** + /// ```dart + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// // JSON Web Key as map representing the decoded JSON. + /// final jwk = { + /// 'kty': 'OKP', + /// 'crv': 'X25519', + /// 'x': 'coeKtJr7mBuIBzGjR_T4OFfuU3Sn85-frLUvxzg5320', + /// }; + /// + /// Future main() async { + /// // Import the public key from decoded JSON. + /// final jsonWebKey = await X25519PublicKey.importJsonWebKey(jwk); + /// + /// // Export the key (print it in same format as it was given). + /// final exportedJsonWebKey = await jsonWebKey.exportJsonWebKey(); + /// print(exportedJsonWebKey); + /// } + /// ``` + /// + /// [1]: https://tools.ietf.org/html/rfc7517 + /// [2]: https://datatracker.ietf.org/doc/html/rfc8037#section-2 + static Future importJsonWebKey( + Map jwk, + ) async { + final impl = await webCryptImpl.x25519PublicKey.importJsonWebKey(jwk); + return X25519PublicKey._(impl); + } + + /// Export X25519 public key as a raw list of bytes. + /// + /// **Example** + /// ```dart + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// // Generate a key-pair. + /// final keyPair = await X25519PrivateKey.generateKey(); + /// + /// // Export the public key as raw bytes. + /// final rawPublicKey = await keyPair.publicKey.exportRawKey(); + Future exportRawKey() => _impl.exportRawKey(); + + /// Export X25519 public key in SPKI format. + /// + /// Returns the DER encoding of the _SubjectPublicKeyInfo structure_ specified + /// in [RFC 5280][1] as a list of bytes. + /// + /// **Example** + /// ```dart + /// import 'package:webcrypto/webcrypto.dart'; + /// import 'package:pem/pem.dart'; + /// + /// // Generate a key-pair. + /// final keyPair = await X25519PrivateKey.generateKey(); + /// + /// // Export the public key. + /// final spkiPublicKey = await keyPair.publicKey.exportSpkiKey(); + /// + /// // Public keys are often encoded as PEM. + /// // This encode the key in base64 and wraps it with: + /// // '-----BEGIN PUBLIC KEY-----'... + /// print(PemCodec(PemLabel.publicKey).encode(spkiPublicKey)); + /// ``` + /// + /// [1]: https://tools.ietf.org/html/rfc5280 + Future exportSpkiKey() => _impl.exportSpkiKey(); + + /// Export the [X25519PublicKey] as a [JSON Web Key][1]. + /// + /// {@macro exportJsonWebKey:returns} + /// + /// **Example** + /// ```dart + /// import 'dart:convert'; + /// import 'package:webcrypto/webcrypto.dart'; + /// + /// // Public JSON Web Key data. + /// final jwk = { + /// 'kty': 'OKP', + /// 'crv': 'X25519', + /// 'x': 'coeKtJr7mBuIBzGjR_T4OFfuU3Sn85-frLUvxzg5320', + /// }; + /// + /// Future main() async { + /// // Import Alice's public key + /// final pkA = await X25519PublicKey.importJsonWebKey(jwk); + /// + /// // Export the public key as a JSON Web Key. + /// final exportedPublicKey = await pkA.exportJsonWebKey(); + /// + /// // The Map returned by `exportJsonWebKey()` can be converted to JSON with + /// // `jsonEncode` from `dart:convert`. + /// print(jsonEncode(exportedPublicKey)); + /// } + /// ``` + /// [1]: https://www.rfc-editor.org/rfc/rfc7518.html + Future> exportJsonWebKey() => _impl.exportJsonWebKey(); +} diff --git a/src/symbols.generated.c b/src/symbols.generated.c index a2d1d6dd..0f78f10a 100644 --- a/src/symbols.generated.c +++ b/src/symbols.generated.c @@ -85,10 +85,12 @@ void* _webcrypto_symbol_table[] = { (void*)&EVP_CipherUpdate, (void*)&EVP_DigestFinal, (void*)&EVP_DigestInit, + (void*)&EVP_DigestSign, (void*)&EVP_DigestSignFinal, (void*)&EVP_DigestSignInit, (void*)&EVP_DigestSignUpdate, (void*)&EVP_DigestUpdate, + (void*)&EVP_DigestVerify, (void*)&EVP_DigestVerifyFinal, (void*)&EVP_DigestVerifyInit, (void*)&EVP_DigestVerifyUpdate, @@ -101,6 +103,7 @@ void* _webcrypto_symbol_table[] = { (void*)&EVP_parse_public_key, (void*)&EVP_PKEY_CTX_free, (void*)&EVP_PKEY_CTX_new, + (void*)&EVP_PKEY_CTX_new_id, (void*)&EVP_PKEY_CTX_set0_rsa_oaep_label, (void*)&EVP_PKEY_CTX_set_rsa_mgf1_md, (void*)&EVP_PKEY_CTX_set_rsa_oaep_md, @@ -108,13 +111,22 @@ void* _webcrypto_symbol_table[] = { (void*)&EVP_PKEY_CTX_set_rsa_pss_saltlen, (void*)&EVP_PKEY_decrypt, (void*)&EVP_PKEY_decrypt_init, + (void*)&EVP_PKEY_derive, + (void*)&EVP_PKEY_derive_init, + (void*)&EVP_PKEY_derive_set_peer, (void*)&EVP_PKEY_encrypt, (void*)&EVP_PKEY_encrypt_init, (void*)&EVP_PKEY_free, + (void*)&EVP_PKEY_get_raw_private_key, + (void*)&EVP_PKEY_get_raw_public_key, (void*)&EVP_PKEY_get1_EC_KEY, (void*)&EVP_PKEY_get1_RSA, (void*)&EVP_PKEY_id, + (void*)&EVP_PKEY_keygen, + (void*)&EVP_PKEY_keygen_init, (void*)&EVP_PKEY_new, + (void*)&EVP_PKEY_new_raw_private_key, + (void*)&EVP_PKEY_new_raw_public_key, (void*)&EVP_PKEY_set1_EC_KEY, (void*)&EVP_PKEY_set1_RSA, (void*)&EVP_sha1, diff --git a/src/symbols.yaml b/src/symbols.yaml index db96c5ac..32a3ecb3 100644 --- a/src/symbols.yaml +++ b/src/symbols.yaml @@ -108,10 +108,12 @@ - EVP_CipherUpdate - EVP_DigestFinal - EVP_DigestInit +- EVP_DigestSign - EVP_DigestSignFinal - EVP_DigestSignInit - EVP_DigestSignUpdate - EVP_DigestUpdate +- EVP_DigestVerify - EVP_DigestVerifyFinal - EVP_DigestVerifyInit - EVP_DigestVerifyUpdate @@ -124,6 +126,7 @@ - EVP_parse_public_key - EVP_PKEY_CTX_free - EVP_PKEY_CTX_new +- EVP_PKEY_CTX_new_id - EVP_PKEY_CTX_set0_rsa_oaep_label - EVP_PKEY_CTX_set_rsa_mgf1_md - EVP_PKEY_CTX_set_rsa_oaep_md @@ -131,13 +134,22 @@ - EVP_PKEY_CTX_set_rsa_pss_saltlen - EVP_PKEY_decrypt - EVP_PKEY_decrypt_init +- EVP_PKEY_derive +- EVP_PKEY_derive_init +- EVP_PKEY_derive_set_peer - EVP_PKEY_encrypt - EVP_PKEY_encrypt_init - EVP_PKEY_free +- EVP_PKEY_get_raw_private_key +- EVP_PKEY_get_raw_public_key - EVP_PKEY_get1_EC_KEY - EVP_PKEY_get1_RSA - EVP_PKEY_id +- EVP_PKEY_keygen +- EVP_PKEY_keygen_init - EVP_PKEY_new +- EVP_PKEY_new_raw_private_key +- EVP_PKEY_new_raw_public_key - EVP_PKEY_set1_EC_KEY - EVP_PKEY_set1_RSA - EVP_sha1 From de5aea462707f865adb8fb9d51e0f32320c61569 Mon Sep 17 00:00:00 2001 From: Tariq Zaid Date: Sat, 11 Oct 2025 18:05:08 +0200 Subject: [PATCH 2/2] re-enable all tests --- lib/src/testing/testing.dart | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/src/testing/testing.dart b/lib/src/testing/testing.dart index 28a5d0e0..0b74d5dc 100644 --- a/lib/src/testing/testing.dart +++ b/lib/src/testing/testing.dart @@ -36,17 +36,17 @@ import 'webcrypto/digest.dart' as digest; /// Test runners from all test files except `digest.dart` and /// `random.dart`, which do not use [TestRunner]. final _testRunners = [ - // aescbc.runner, - // aesctr.runner, - // aesgcm.runner, - // ecdh.runner, - // ecdsa.runner, - // hkdf.runner, - // hmac.runner, - // pbkdf2.runner, - // rsaoaep.runner, - // rsapss.runner, - // rsassapkcs1v15.runner, + aescbc.runner, + aesctr.runner, + aesgcm.runner, + ecdh.runner, + ecdsa.runner, + hkdf.runner, + hmac.runner, + pbkdf2.runner, + rsaoaep.runner, + rsapss.runner, + rsassapkcs1v15.runner, ed25519.runner, x25519.runner, ];