diff --git a/lib/src/impl_ffi/impl_ffi.hmac.dart b/lib/src/impl_ffi/impl_ffi.hmac.dart index 71780f08..f48135d4 100644 --- a/lib/src/impl_ffi/impl_ffi.hmac.dart +++ b/lib/src/impl_ffi/impl_ffi.hmac.dart @@ -43,6 +43,15 @@ Future hmacSecretKey_importRawKey( ); } +void _checkHmacLength(List keyData, int length) { + _checkData( + length >= 0 && + length <= keyData.length * 8 && + length > (keyData.length - 1) * 8, + message: 'JWK property "k" does not match expected length', + ); +} + Future hmacSecretKey_importJsonWebKey( Map jwk, HashImpl hash, { @@ -65,6 +74,9 @@ Future hmacSecretKey_importJsonWebKey( ); final keyData = _jwkDecodeBase64UrlNoPadding(k.k!, 'k'); + if (length != null) { + _checkHmacLength(keyData, length); + } return hmacSecretKey_importRawKey(keyData, hash, length: length); } diff --git a/lib/src/testing/regression/issue_302_hmac_jwk_length.dart b/lib/src/testing/regression/issue_302_hmac_jwk_length.dart new file mode 100644 index 00000000..38d02f98 --- /dev/null +++ b/lib/src/testing/regression/issue_302_hmac_jwk_length.dart @@ -0,0 +1,65 @@ +// Copyright 2026 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'; + +void main() => tests().runTests(); + +List<({String name, Future Function() test})> tests() { + final tests = <({String name, Future Function() test})>[]; + void test(String name, Future Function() fn) => + tests.add((name: name, test: fn)); + + test('Hmac: importJsonWebKey applies length like importRawKey', () async { + final keyData = [0xff, 0xe0]; + final jwk = {'kty': 'oct', 'alg': 'HS256', 'k': '_-A'}; + + final rawKey = await HmacSecretKey.importRawKey( + keyData, + Hash.sha256, + length: 9, + ); + final jwkKey = await HmacSecretKey.importJsonWebKey( + jwk, + Hash.sha256, + length: 9, + ); + + check( + equalBytes(await rawKey.exportRawKey(), await jwkKey.exportRawKey()), + 'JWK import should zero unused bits the same way as raw import', + ); + + final data = [1, 2, 3, 4]; + check( + equalBytes(await rawKey.signBytes(data), await jwkKey.signBytes(data)), + 'JWK import should use the requested HMAC key length for signing', + ); + }); + + test('Hmac: importJsonWebKey validates length', () async { + final jwk = {'kty': 'oct', 'alg': 'HS256', 'k': '_4A'}; + + bool threw = false; + try { + await HmacSecretKey.importJsonWebKey(jwk, Hash.sha256, length: 7); + } on FormatException { + threw = true; + } + check(threw, 'Should throw FormatException for invalid HMAC key length'); + }); + + return tests; +} diff --git a/lib/src/testing/testing.dart b/lib/src/testing/testing.dart index 3c7b4fd2..a9a47bf2 100644 --- a/lib/src/testing/testing.dart +++ b/lib/src/testing/testing.dart @@ -30,6 +30,7 @@ import 'webcrypto/rsassapkcs1v15.dart' as rsassapkcs1v15; // Other test files, that don't use TestRunner import 'webcrypto/random.dart' as random; import 'webcrypto/digest.dart' as digest; +import 'regression/issue_302_hmac_jwk_length.dart' as issue_302_hmac_jwk_length; import 'regression/issue_60_trailing_bytes.dart' as issue_60_trailing_bytes; /// Test runners from all test files except `digest.dart` and @@ -59,6 +60,7 @@ void runAllTests( for (final r in _testRunners) ...r.tests(), ...random.tests(), ...digest.tests(), + ...issue_302_hmac_jwk_length.tests(), ...issue_60_trailing_bytes.tests(), ]; diff --git a/lib/src/webcrypto/webcrypto.hmac.dart b/lib/src/webcrypto/webcrypto.hmac.dart index 0fa30a93..0ebbe4f9 100644 --- a/lib/src/webcrypto/webcrypto.hmac.dart +++ b/lib/src/webcrypto/webcrypto.hmac.dart @@ -53,6 +53,24 @@ final class HmacSecretKey { HmacSecretKey._(this._impl); // keep the constructor private. + static void _checkLength(int length, int keyDataLength) { + if (length > keyDataLength * 8) { + throw ArgumentError.value( + length, + 'length', + 'must be less than number of bits in keyData', + ); + } + if (length <= (keyDataLength - 1) * 8) { + throw ArgumentError.value( + length, + 'length', + 'must be greater than number of bits in keyData - 8, you can attain ' + 'the same effect by removing bytes from keyData', + ); + } + } + /// Import [HmacSecretKey] from raw [keyData]. /// /// Creates an [HmacSecretKey] using [keyData] as secret key, and running @@ -82,20 +100,8 @@ final class HmacSecretKey { }) async { // These limitations are given in Web Cryptography Spec: // https://www.w3.org/TR/WebCryptoAPI/#hmac-operations - if (length != null && length > keyData.length * 8) { - throw ArgumentError.value( - length, - 'length', - 'must be less than number of bits in keyData', - ); - } - if (length != null && length <= (keyData.length - 1) * 8) { - throw ArgumentError.value( - length, - 'length', - 'must be greater than number of bits in keyData - 8, you can attain ' - 'the same effect by removing bytes from keyData', - ); + if (length != null) { + _checkLength(length, keyData.length); } final impl = await webCryptImpl.hmacSecretKey.importRawKey( @@ -162,26 +168,10 @@ final class HmacSecretKey { Hash hash, { int? length, }) async { - /* - TODO: Validate these in the native implememtation - // These limitations are given in Web Cryptography Spec: - // https://www.w3.org/TR/WebCryptoAPI/#hmac-operations - if (length != null && length > keyData.length * 8) { - throw ArgumentError.value( - length, 'length', 'must be less than number of bits in keyData'); - } - if (length != null && length <= (keyData.length - 1) * 8) { - throw ArgumentError.value( - length, - 'length', - 'must be greater than number of bits in keyData - 8, you can attain ' - 'the same effect by removing bytes from keyData', - ); - }*/ - final impl = await webCryptImpl.hmacSecretKey.importJsonWebKey( jwk, hash._impl, + length: length, ); return HmacSecretKey._(impl);