Summary
The native FFI backend silently narrows oversized integer parameters before passing them to BoringSSL. This can change security-sensitive parameters modulo 2^32 instead of rejecting them.
Two affected operations are:
Pbkdf2SecretKey.deriveBits: iterations = 2^32 + 1 is executed as one iteration.
- RSA-PSS signing and verification:
saltLength = 2^32 is executed as a zero-byte salt.
The browser backend rejects these values during Web IDL normalization because both parameters are defined as [EnforceRange] unsigned long.
Reproduction
import 'package:webcrypto/webcrypto.dart';
Future<void> main() async {
final password = await Pbkdf2SecretKey.importRawKey([1, 2, 3, 4]);
final once = await password.deriveBits(
256,
Hash.sha256,
[5, 6, 7, 8],
1,
);
final wrappedIterations = await password.deriveBits(
256,
Hash.sha256,
[5, 6, 7, 8],
0x100000001,
);
print(_equal(once, wrappedIterations));
final pair = await RsaPssPrivateKey.generateKey(
1024,
BigInt.from(65537),
Hash.sha256,
);
final message = [1, 2, 3, 4];
final zeroSalt = await pair.privateKey.signBytes(message, 0);
final wrappedSalt = await pair.privateKey.signBytes(
message,
0x100000000,
);
print(_equal(zeroSalt, wrappedSalt));
print(await pair.publicKey.verifyBytes(wrappedSalt, message, 0));
}
bool _equal(List<int> a, List<int> b) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
Native output:
The PBKDF2 call completes immediately and produces exactly the same derived bits as one iteration. The RSA-PSS signature produced with saltLength = 2^32 is exactly the deterministic zero-salt signature and verifies when saltLength is specified as zero.
Expected behavior
Values outside the Web Crypto unsigned long range (0 through 2^32 - 1) should be rejected before reaching FFI.
The Web Cryptography specification defines:
Pbkdf2Params.iterations as required [EnforceRange] unsigned long.
RsaPssParams.saltLength as required [EnforceRange] unsigned long.
https://www.w3.org/TR/WebCryptoAPI/#dfn-Pbkdf2Params
https://www.w3.org/TR/WebCryptoAPI/#dfn-RsaPssParams
Actual behavior
The native implementations only validate the lower bounds:
- PBKDF2 rejects
iterations <= 0.
- RSA-PSS rejects
saltLength < 0.
The values are then passed to 32-bit native parameters, where oversized Dart integers are narrowed modulo 2^32.
This creates a backend difference: standards-based Web Crypto implementations reject the same inputs with a range error, while the native backend can successfully perform a different and substantially weaker operation.
Impact
The PBKDF2 case is security-relevant. An application can believe it requested a very expensive password-hardening operation while the native backend performs only a small wrapped iteration count. For example, 2^32 + 1 becomes one iteration.
For RSA-PSS, an oversized salt length can become a smaller salt length, including zero, changing the signature scheme parameters without notifying the caller.
Suggested fix
- Validate PBKDF2
iterations as 1 <= iterations <= 0xffffffff before the FFI call.
- Validate RSA-PSS
saltLength as 0 <= saltLength <= 0xffffffff before signing or verification.
- Add native regression tests at
2^32, 2^32 + 1, and valid boundary-adjacent values.
- Audit other Dart integers passed to fixed-width native parameters for the same narrowing behavior.
Summary
The native FFI backend silently narrows oversized integer parameters before passing them to BoringSSL. This can change security-sensitive parameters modulo
2^32instead of rejecting them.Two affected operations are:
Pbkdf2SecretKey.deriveBits:iterations = 2^32 + 1is executed as one iteration.saltLength = 2^32is executed as a zero-byte salt.The browser backend rejects these values during Web IDL normalization because both parameters are defined as
[EnforceRange] unsigned long.Reproduction
Native output:
The PBKDF2 call completes immediately and produces exactly the same derived bits as one iteration. The RSA-PSS signature produced with
saltLength = 2^32is exactly the deterministic zero-salt signature and verifies whensaltLengthis specified as zero.Expected behavior
Values outside the Web Crypto
unsigned longrange (0through2^32 - 1) should be rejected before reaching FFI.The Web Cryptography specification defines:
Pbkdf2Params.iterationsasrequired [EnforceRange] unsigned long.RsaPssParams.saltLengthasrequired [EnforceRange] unsigned long.https://www.w3.org/TR/WebCryptoAPI/#dfn-Pbkdf2Params
https://www.w3.org/TR/WebCryptoAPI/#dfn-RsaPssParams
Actual behavior
The native implementations only validate the lower bounds:
iterations <= 0.saltLength < 0.The values are then passed to 32-bit native parameters, where oversized Dart integers are narrowed modulo
2^32.This creates a backend difference: standards-based Web Crypto implementations reject the same inputs with a range error, while the native backend can successfully perform a different and substantially weaker operation.
Impact
The PBKDF2 case is security-relevant. An application can believe it requested a very expensive password-hardening operation while the native backend performs only a small wrapped iteration count. For example,
2^32 + 1becomes one iteration.For RSA-PSS, an oversized salt length can become a smaller salt length, including zero, changing the signature scheme parameters without notifying the caller.
Suggested fix
iterationsas1 <= iterations <= 0xffffffffbefore the FFI call.saltLengthas0 <= saltLength <= 0xffffffffbefore signing or verification.2^32,2^32 + 1, and valid boundary-adjacent values.