Skip to content

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

Description

@harrshita123

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:

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions