google / google/webcrypto.dart
bug: native FFI wraps oversized PBKDF2 iterations and RSA-PSS salt lengths
- Dominant language
- Dart
- Stars
- 116
- Forks
- 110
- Avg merge
- 6d 8h
- Merged PRs (30d)
- 9
Description
## 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
```dart
import 'package:webcrypto/webcrypto.dart';
Future 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 a, List 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:
```text
true
true
true
```
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.
Contributor guide
Research direction
Locate the native implementations of Pbkdf2SecretKey.deriveBits and RSA-PSS signing and verification, then inspect how iterations and saltLength are passed through FFI. Add regression coverage for 2^32, 2^32 + 1, and boundary-adjacent valid values; done means oversized values are rejected before the native call and valid values retain their behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- cryptography, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 57/100