Skip to content

bug: browser ECDH deriveBits wraps invalid lengths at the WebIDL boundary #366

Description

@harrshita123

Description

The browser ECDH backend rounds length up to a whole number of bytes before calling SubtleCrypto.deriveBits, but it does not validate the value first. Near the WebIDL unsigned long boundary, the rounded value wraps before the browser performs the curve-specific length check.

This creates backend-dependent behavior for inputs that are outside the documented ECDH limit:

  • The native backend rejects them.
  • Chrome under both dart2js and dart2wasm can throw an unrelated StateError, return an empty result, or return a one-bit result.

Reproduction

import 'package:webcrypto/webcrypto.dart';

Future<void> main() async {
  final alice = await EcdhPrivateKey.generateKey(EllipticCurve.p256);
  final bob = await EcdhPrivateKey.generateKey(EllipticCurve.p256);

  for (final length in <int>[
    -1,
    0xfffffff9,
    0xffffffff,
    0x100000000,
    0x100000001,
  ]) {
    try {
      final secret = await alice.privateKey.deriveBits(
        length,
        bob.publicKey,
      );
      print('$length: accepted with ${secret.length} bytes');
    } catch (error) {
      print('$length: ${error.runtimeType}: $error');
    }
  }
}

Native output:

-1: ArgumentError
4294967289: OperationError
4294967295: OperationError
4294967296: OperationError
4294967297: OperationError

Chrome output under both dart2js and dart2wasm:

-1: StateError: Bad state: No element
4294967289: StateError: Bad state: No element
4294967295: StateError: Bad state: No element
4294967296: accepted with 0 bytes
4294967297: accepted with 1 bytes

Expected behavior

Invalid ECDH output lengths should be rejected consistently before browser numeric conversion and before indexing the derived result. In particular, values outside the selected curve's maximum must not wrap into an empty or one-bit derivation, and invalid negative values must not surface as StateError.

Valid zero-length and non-byte-aligned derivations should continue to work.

Cause

The shared public method forwards length without validation. The browser implementation then calculates:

final lengthInBytes = (length / 8).ceil();

and passes lengthInBytes * 8 to SubtleCrypto.deriveBits. Values such as 0xfffffff9 through 0xffffffff round to 0x100000000, which WebIDL converts to zero for the browser call. The wrapper then accesses derived.last while applying its non-byte-aligned mask, causing StateError. Values at and above 0x100000000 can similarly wrap to small successful derivations.

The native implementation validates negative and curve-specific maximum lengths before deriving, so it does not exhibit this behavior.

Suggested fix

  • Validate ECDH length before the browser backend rounds it or passes it through WebIDL conversion.
  • Preserve the curve-specific maximums: 256 bits for P-256, 384 bits for P-384, and 528 bits for P-521.
  • Add shared regression coverage for negative values, the first value above each curve maximum, 0xfffffff9, 0xffffffff, 0x100000000, and 0x100000001.
  • Run the regression coverage on native, Chrome/dart2js, and Chrome/dart2wasm.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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