google / google/webcrypto.dart

bug: native FFI wraps oversized PBKDF2 iterations and RSA-PSS salt lengths

Open
#347 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.