From 634c809c5a6ce4066960dfe9bea416d27f116aad Mon Sep 17 00:00:00 2001 From: Harshita Yadav Date: Wed, 29 Jul 2026 17:39:00 +0530 Subject: [PATCH] fix(hkdf): validate output length before allocation --- CHANGELOG.md | 2 + lib/src/impl_ffi/impl_ffi.hkdf.dart | 10 ++-- test/hkdf_output_length_test.dart | 79 +++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 test/hkdf_output_length_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index d80865a57..e0cdf0ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # 0.6.2-wip * Fixed JS interop to enable WebAssembly. * Fixed native RSA public-key JWK imports to reject private key material. +* Fixed native HKDF output-length validation to reject oversized requests + before allocating memory. # 0.6.1 * Added Dart native build hooks and native asset lookup for the bundled diff --git a/lib/src/impl_ffi/impl_ffi.hkdf.dart b/lib/src/impl_ffi/impl_ffi.hkdf.dart index 9ea1cb6b4..b115de8bf 100644 --- a/lib/src/impl_ffi/impl_ffi.hkdf.dart +++ b/lib/src/impl_ffi/impl_ffi.hkdf.dart @@ -57,6 +57,12 @@ final class _HkdfSecretKeyImpl implements HkdfSecretKeyImpl { } final lengthInBytes = length ~/ 8; + const lengthTooLong = + 'Length specified for HkdfSecretKey.deriveBits is too long'; + final maxLengthInBytes = 255 * ssl.EVP_MD_size(md); + if (lengthInBytes > maxLengthInBytes) { + throw operationError(lengthTooLong); + } return _Scope.async((scope) async { final out = scope(lengthInBytes); @@ -76,9 +82,7 @@ final class _HkdfSecretKeyImpl implements HkdfSecretKeyImpl { if (ERR_GET_LIB(packed_error) == ERR_LIB_HKDF && ERR_GET_REASON(packed_error) == HKDF_R_OUTPUT_TOO_LARGE) { ssl.ERR_clear_error(); - throw operationError( - 'Length specified for HkdfSecretKey.deriveBits is too long', - ); + throw operationError(lengthTooLong); } _checkOpIsOne(r, fallback: 'HKDF key derivation failed'); } diff --git a/test/hkdf_output_length_test.dart b/test/hkdf_output_length_test.dart new file mode 100644 index 000000000..3860e3608 --- /dev/null +++ b/test/hkdf_output_length_test.dart @@ -0,0 +1,79 @@ +// 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. + +@TestOn('vm') +library; + +import 'package:test/test.dart'; +import 'package:webcrypto/webcrypto.dart'; + +const _lengthTooLong = + 'Length specified for HkdfSecretKey.deriveBits is too long'; + +void main() { + late HkdfSecretKey key; + + setUpAll(() async { + key = await HkdfSecretKey.importRawKey(const [1, 2, 3, 4]); + }); + + for (final (name, hash, hashLengthInBytes) in [ + ('SHA-1', Hash.sha1, 20), + ('SHA-256', Hash.sha256, 32), + ('SHA-384', Hash.sha384, 48), + ('SHA-512', Hash.sha512, 64), + ]) { + final maxLengthInBytes = 255 * hashLengthInBytes; + + test('$name accepts the RFC 5869 maximum output length', () async { + final derived = await key.deriveBits( + maxLengthInBytes * 8, + hash, + const [], + const [], + ); + + expect(derived, hasLength(maxLengthInBytes)); + }); + + test('$name rejects output one byte beyond the maximum', () async { + await expectLater( + key.deriveBits((maxLengthInBytes + 1) * 8, hash, const [], const []), + throwsA( + isA().having( + (error) => error.toString(), + 'message', + _lengthTooLong, + ), + ), + ); + }); + } + + test( + 'rejects a huge output length before allocating native memory', + () async { + await expectLater( + key.deriveBits(9223372036854775800, Hash.sha256, const [], const []), + throwsA( + isA().having( + (error) => error.toString(), + 'message', + _lengthTooLong, + ), + ), + ); + }, + ); +}