Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions lib/src/impl_ffi/impl_ffi.aes_common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ Uint8List _aesImportJwkKey(
checkJwk(k.kty == 'oct', 'kty', 'must be "oct"');
checkJwk(k.k != null, 'k', 'must be present');
checkJwk(k.use == null || k.use == 'enc', 'use', 'must be "enc", if present');
if (k.key_ops != null) {
const allowedOps = <String>{
'encrypt',
'decrypt',
'wrapKey',
'unwrapKey',
'deriveKey',
'deriveBits',
};
for (final op in k.key_ops!) {
checkJwk(
allowedOps.contains(op),
op,
'is not consistent with use "enc"',
);
}
}

final keyData = _jwkDecodeBase64UrlNoPadding(k.k!, 'k');
if (keyData.length == 24) {
Expand Down
22 changes: 19 additions & 3 deletions lib/src/impl_ffi/impl_ffi.ec_common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,25 @@ _EvpPKey _importJwkEcPrivateOrPublicKey(
jwk.use == null || jwk.use == expectedUse,
message: 'JWK property "use" should be "$expectedUse", if present',
);

// TODO: Reject keys with key_ops in inconsistent with isPrivateKey
// Also in the js implementation...
if (jwk.key_ops != null) {
final allowedOps = expectedUse == 'sig'
? <String>{'sign', 'verify'}
: <String>{
'encrypt',
'decrypt',
'wrapKey',
'unwrapKey',
'deriveKey',
'deriveBits',
};
for (final op in jwk.key_ops!) {
_checkData(
allowedOps.contains(op),
message: 'JWK key_ops entry "$op" is not consistent with use '
'"$expectedUse"',
);
}
}

return _Scope.sync((scope) {
final ec = ssl.EC_KEY_new_by_curve_name(_ecCurveToNID(curve));
Expand Down
10 changes: 10 additions & 0 deletions lib/src/impl_ffi/impl_ffi.hmac.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ Future<HmacSecretKeyImpl> hmacSecretKey_importJsonWebKey(
checkJwk(k.kty == 'oct', 'kty', 'must be "oct"');
checkJwk(k.k != null, 'k', 'must be present');
checkJwk(k.use == null || k.use == 'sig', 'use', 'must be "sig", if present');
if (k.key_ops != null) {
const allowedOps = <String>{'sign', 'verify'};
for (final op in k.key_ops!) {
checkJwk(
allowedOps.contains(op),
op,
'is not consistent with use "sig"',
);
}
}
final expectedAlg = h.hmacJwkAlg;
checkJwk(
k.alg == null || k.alg == expectedAlg,
Expand Down
26 changes: 19 additions & 7 deletions lib/src/impl_ffi/impl_ffi.rsa_common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,25 @@ _EvpPKey _importJwkRsaPrivateOrPublicKey(
'use',
'must be "$expectedUse", if present',
);

// TODO: Consider rejecting private keys with 'use', as it's only valid for
// public keys according to the RFC -- maybe read the RFC again to be
// perfectly sure this is correct behavior.
// Also the web crypto spec on this, which says to reject invalid 'use'
// TODO: Consider rejecting keys with key_ops inconsistent with isPrivateKey
// See also JWK import logic for EC keys
if (jwk.key_ops != null) {
final allowedOps = expectedUse == 'sig'
? <String>{'sign', 'verify'}
: <String>{
'encrypt',
'decrypt',
'wrapKey',
'unwrapKey',
'deriveKey',
'deriveBits',
};
for (final op in jwk.key_ops!) {
checkJwk(
allowedOps.contains(op),
op,
'is not consistent with use "$expectedUse"',
);
}
}

ffi.Pointer<BIGNUM> readBN(String value, String prop) {
final bin = _jwkDecodeBase64UrlNoPadding(value, prop);
Expand Down
12 changes: 0 additions & 12 deletions lib/src/impl_js/impl_js.ecdh.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,6 @@ Future<EcdhPrivateKeyImpl> ecdhPrivateKey_importJsonWebKey(
Map<String, dynamic> jwk,
EllipticCurve curve,
) async {
if (jwk['use'] == 'enc') {
// Chrome incorrectly forbids the 'enc' value for ECDH, hence, we strip it
// when importing to ensure compatibility across browsers.
// See: https://crbug.com/641499
jwk = Map.fromEntries(jwk.entries.where((e) => e.key != 'use'));
}
return _EcdhPrivateKeyImpl(
await _importJsonWebKey(
jwk,
Expand Down Expand Up @@ -111,12 +105,6 @@ Future<EcdhPublicKeyImpl> ecdhPublicKey_importJsonWebKey(
Map<String, dynamic> jwk,
EllipticCurve curve,
) async {
if (jwk['use'] == 'enc') {
// Chrome incorrectly forbids the 'enc' value for ECDH, hence, we strip it
// when importing to ensure compatibility across browsers.
// See: https://crbug.com/641499
jwk = Map.fromEntries(jwk.entries.where((e) => e.key != 'use'));
}
return _EcdhPublicKeyImpl(
await _importJsonWebKey(
jwk,
Expand Down
40 changes: 36 additions & 4 deletions lib/src/impl_js/impl_js.utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,34 @@ final _usagesDecrypt = ['decrypt'];
final _usagesEncrypt = ['encrypt'];
final _usagesDeriveBits = ['deriveBits'];

/// Validate that [jwk]'s `use` and `key_ops` are consistent with each other.
///
/// If both `use` and `key_ops` are present, every entry in `key_ops` must be
/// consistent with the value of `use` per the JWK specification (RFC 7517).
void _checkJwkUseAndKeyOps(subtle.JsonWebKey jwk) {
if (jwk.use != null && jwk.key_ops != null) {
final allowedOps = jwk.use == 'sig'
? <String>{'sign', 'verify'}
: <String>{
'encrypt',
'decrypt',
'wrapKey',
'unwrapKey',
'deriveKey',
'deriveBits',
};
for (final op in jwk.key_ops!) {
if (!allowedOps.contains(op)) {
throw ArgumentError.value(
jwk,
'jwk',
'key_ops contains "$op" which is inconsistent with use "${jwk.use}"',
);
}
}
}
}

/// Adapt `crypto.subtle.importKey` to Dart types for JWK.
Future<subtle.JSCryptoKey> _importJsonWebKey(
Map<String, dynamic> jwk,
Expand All @@ -156,11 +184,15 @@ Future<subtle.JSCryptoKey> _importJsonWebKey(
) {
return _handleDomException(() async {
final jwkObj = subtle.JsonWebKey.fromJson(jwk);
// TODO: Validate expected 'use' the way we have it in the FFI implementation

// Remove 'key_ops' and 'ext' as this library doesn't configuring
// _usages_ and _extractable_.
// Notice that we also strip 'key_ops' and 'ext' in [_exportJsonWebKey].
// Validate consistency of 'use' and 'key_ops' before stripping.
_checkJwkUseAndKeyOps(jwkObj);

// Remove 'use', 'key_ops', and 'ext' as this library doesn't configure
// _usages_ and _extractable_. The browser's usages are driven by the
// explicit `usages` parameter passed to importKey.
// Notice that we also strip these in [_exportJsonWebKey].
jwkObj.use = null;
jwkObj.key_ops = null;
jwkObj.ext = null;
final k = await subtle.importJsonWebKey(
Expand Down
81 changes: 81 additions & 0 deletions test/jwk_key_ops_validation_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'package:test/test.dart';
import 'package:webcrypto/webcrypto.dart';

void main() {
group('JWK import rejects conflicting use and key_ops', () {
test('HMAC: use=sig with key_ops=[encrypt]', () async {
await expectLater(
() => HmacSecretKey.importJsonWebKey(
{
'kty': 'oct',
'k': 'YWJjZGVmZ2hpamtsbW5vcA',
'use': 'sig',
'key_ops': ['encrypt'],
},
Hash.sha256,
),
throwsA(anything),
);
});

test('AES: use=enc with key_ops=[sign]', () async {
await expectLater(
() => AesCbcSecretKey.importJsonWebKey(
{
'kty': 'oct',
'k': 'YWJjZGVmZ2hpamtsbW5vcA',
'use': 'enc',
'key_ops': ['sign'],
},
),
throwsA(anything),
);
});

test('ECDSA: use=sig with key_ops=[deriveBits]', () async {
await expectLater(
() => EcdsaPrivateKey.importJsonWebKey(
{
'kty': 'EC',
'crv': 'P-256',
'x': 'MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4',
'y': '4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM',
'd': '870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE',
'use': 'sig',
'key_ops': ['deriveBits'],
},
EllipticCurve.p256,
),
throwsA(anything),
);
});

test('RSA: use=sig with key_ops=[encrypt,decrypt]', () async {
await expectLater(
() => RsaSsaPkcs1V15PrivateKey.importJsonWebKey(
{
'kty': 'RSA',
'use': 'sig',
'key_ops': ['encrypt', 'decrypt'],
},
Hash.sha256,
),
throwsA(anything),
);
});
});
}