Skip to content

Commit

Permalink
crypto: allow non-multiple of 8 in SubtleCrypto.deriveBits
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Oct 6, 2024
1 parent 36ca010 commit 55938d2
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 17 deletions.
3 changes: 0 additions & 3 deletions doc/api/webcrypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,9 +600,6 @@ Using the method and parameters specified in `algorithm` and the keying
material provided by `baseKey`, `subtle.deriveBits()` attempts to generate
`length` bits.

The Node.js implementation requires that `length`, when a number, is a multiple
of `8`.

When `length` is not provided or `null` the maximum number of bits for a given
algorithm is generated. This is allowed for the `'ECDH'`, `'X25519'`, and `'X448'`
algorithms, for other algorithms `length` is required to be a number.
Expand Down
44 changes: 36 additions & 8 deletions lib/internal/crypto/diffiehellman.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
MathCeil,
ObjectDefineProperty,
SafeSet,
Uint8Array,
} = primordials;

const { Buffer } = require('buffer');
Expand Down Expand Up @@ -342,18 +343,45 @@ async function ecdhDeriveBits(algorithm, baseKey, length) {

// If the length is not a multiple of 8 the nearest ceiled
// multiple of 8 is sliced.
length = MathCeil(length / 8);
const { byteLength } = bits;
const sliceLength = MathCeil(length / 8);

const { byteLength } = bits;
// If the length is larger than the derived secret, throw.
// Otherwise, we either return the secret or a truncated
// slice.
if (byteLength < length)
if (byteLength < sliceLength)
throw lazyDOMException('derived bit length is too small', 'OperationError');

return length === byteLength ?
bits :
ArrayBufferPrototypeSlice(bits, 0, length);
const slice = ArrayBufferPrototypeSlice(bits, 0, sliceLength);

let mask;
switch (length % 8) {
case 0:
return slice;
case 1:
mask = 0b10000000;
break;
case 2:
mask = 0b11000000;
break;
case 3:
mask = 0b11100000;
break;
case 4:
mask = 0b11110000;
break;
case 5:
mask = 0b11111000;
break;
case 6:
mask = 0b11111100;
break;
case 7:
mask = 0b11111110;
break;
}

const masked = new Uint8Array(slice);
masked[sliceLength - 1] = masked[sliceLength - 1] & mask;
return masked.buffer;
}

module.exports = {
Expand Down
8 changes: 5 additions & 3 deletions test/parallel/test-webcrypto-derivebits-cfrg.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,11 @@ async function prepareKeys() {
public: publicKey
}, privateKey, 8 * size - 11);

assert.strictEqual(
Buffer.from(bits).toString('hex'),
result.slice(0, -2));
const expected = Buffer.from(result.slice(0, -2), 'hex');
expected[size - 2] = expected[size - 2] & 0b11111000;
assert.deepStrictEqual(
Buffer.from(bits),
expected);
}
}));

Expand Down
8 changes: 5 additions & 3 deletions test/parallel/test-webcrypto-derivebits-ecdh.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,11 @@ async function prepareKeys() {
public: publicKey
}, privateKey, 8 * size - 11);

assert.strictEqual(
Buffer.from(bits).toString('hex'),
result.slice(0, -2));
const expected = Buffer.from(result.slice(0, -2), 'hex');
expected[size - 2] = expected[size - 2] & 0b11111000;
assert.deepStrictEqual(
Buffer.from(bits),
expected);
}
}));

Expand Down

0 comments on commit 55938d2

Please sign in to comment.