diff --git a/lib/src/impl_ffi/impl_ffi.digest.dart b/lib/src/impl_ffi/impl_ffi.digest.dart index c24f25c1..9d9cd288 100644 --- a/lib/src/impl_ffi/impl_ffi.digest.dart +++ b/lib/src/impl_ffi/impl_ffi.digest.dart @@ -74,7 +74,7 @@ abstract class _HashImpl implements HashImpl { /// Algorithm (`alg` for JWK) when this hash algorithm is used in RSA-OAEP. /// - /// For SHA-1, it returns 'RSA-OAEP-1'. + /// For SHA-1, it returns 'RSA-OAEP'. /// For SHA-256, it returns 'RSA-OAEP-256'. /// For SHA-384, it returns 'RSA-OAEP-384'. /// For SHA-512, it returns 'RSA-OAEP-512'. @@ -113,7 +113,7 @@ final class _Sha1 extends _HashImpl { String get hmacJwkAlg => 'HS1'; @override - String get rsaOaepJwkAlg => 'RSA-OAEP-1'; + String get rsaOaepJwkAlg => 'RSA-OAEP'; @override String get rsaPssJwkAlg => 'PS1'; diff --git a/lib/src/testing/regression/rsa_oaep_sha1_jwk_alg.dart b/lib/src/testing/regression/rsa_oaep_sha1_jwk_alg.dart new file mode 100644 index 00000000..8c902325 --- /dev/null +++ b/lib/src/testing/regression/rsa_oaep_sha1_jwk_alg.dart @@ -0,0 +1,107 @@ +// 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 'dart:typed_data'; + +import 'package:webcrypto/webcrypto.dart'; + +import '../utils/utils.dart'; + +List<({String name, Future Function() test})> tests() => [ + ( + name: 'RSA-OAEP SHA-1 uses the canonical JWK alg identifier', + test: () async { + final keyPair = await RsaOaepPrivateKey.generateKey( + 2048, + BigInt.from(65537), + Hash.sha1, + ); + final privateJwk = await keyPair.privateKey.exportJsonWebKey(); + final publicJwk = await keyPair.publicKey.exportJsonWebKey(); + + check( + privateJwk['alg'] == 'RSA-OAEP', + 'Expected private JWK alg to be "RSA-OAEP"', + ); + check( + publicJwk['alg'] == 'RSA-OAEP', + 'Expected public JWK alg to be "RSA-OAEP"', + ); + + final privateKey = await RsaOaepPrivateKey.importJsonWebKey( + privateJwk, + Hash.sha1, + ); + final publicKey = await RsaOaepPublicKey.importJsonWebKey( + publicJwk, + Hash.sha1, + ); + await _checkRoundTrip(privateKey, publicKey); + + final legacyPrivateKey = await RsaOaepPrivateKey.importJsonWebKey({ + ...privateJwk, + 'alg': 'RSA-OAEP-1', + }, Hash.sha1); + final legacyPublicKey = await RsaOaepPublicKey.importJsonWebKey({ + ...publicJwk, + 'alg': 'RSA-OAEP-1', + }, Hash.sha1); + await _checkRoundTrip(legacyPrivateKey, legacyPublicKey); + + for (final (:hash, :name) in [ + (hash: Hash.sha256, name: 'SHA-256'), + (hash: Hash.sha384, name: 'SHA-384'), + (hash: Hash.sha512, name: 'SHA-512'), + ]) { + await _expectFormatException( + () => RsaOaepPrivateKey.importJsonWebKey({ + ...privateJwk, + 'alg': 'RSA-OAEP-1', + }, hash), + 'Expected private RSA-OAEP-1 JWK to be rejected for $name', + ); + await _expectFormatException( + () => RsaOaepPublicKey.importJsonWebKey({ + ...publicJwk, + 'alg': 'RSA-OAEP-1', + }, hash), + 'Expected public RSA-OAEP-1 JWK to be rejected for $name', + ); + } + }, + ), +]; + +Future _checkRoundTrip( + RsaOaepPrivateKey privateKey, + RsaOaepPublicKey publicKey, +) async { + final plaintext = Uint8List.fromList([1, 2, 3, 4]); + final ciphertext = await publicKey.encryptBytes(plaintext); + final decrypted = await privateKey.decryptBytes(ciphertext); + check(equalBytes(decrypted, plaintext), 'RSA-OAEP round trip failed'); +} + +Future _expectFormatException( + Future Function() callback, + String message, +) async { + var rejected = false; + try { + await callback(); + } on FormatException { + rejected = true; + } + check(rejected, message); +} diff --git a/lib/src/testing/testing.dart b/lib/src/testing/testing.dart index 075a7352..21db1cb5 100644 --- a/lib/src/testing/testing.dart +++ b/lib/src/testing/testing.dart @@ -32,6 +32,7 @@ import 'webcrypto/random.dart' as random; import 'webcrypto/digest.dart' as digest; import 'regression/derive_bits_zero_length.dart' as derive_bits_zero_length; import 'regression/issue_60_trailing_bytes.dart' as issue_60_trailing_bytes; +import 'regression/rsa_oaep_sha1_jwk_alg.dart' as rsa_oaep_sha1_jwk_alg; /// Test runners from all test files except `digest.dart` and /// `random.dart`, which do not use [TestRunner]. @@ -62,6 +63,7 @@ void runAllTests( ...digest.tests(), ...issue_60_trailing_bytes.tests(), ...derive_bits_zero_length.tests(), + ...rsa_oaep_sha1_jwk_alg.tests(), ]; for (final (:name, :test) in allTests) { diff --git a/lib/src/webcrypto/webcrypto.rsaoaep.dart b/lib/src/webcrypto/webcrypto.rsaoaep.dart index 0cc715b5..65b1ebf7 100644 --- a/lib/src/webcrypto/webcrypto.rsaoaep.dart +++ b/lib/src/webcrypto/webcrypto.rsaoaep.dart @@ -14,6 +14,17 @@ part of 'webcrypto.dart'; +Map _normalizeLegacyRsaOaepJwkAlg( + Map jwk, + Hash hash, +) { + // Preserve imports of JWKs exported by older FFI releases. + if (identical(hash, Hash.sha1) && jwk['alg'] == 'RSA-OAEP-1') { + return {...jwk, 'alg': 'RSA-OAEP'}; + } + return jwk; +} + /// RSAES-OAEP private key for decryption of messages. /// /// An [RsaOaepPrivateKey] instance holds a private RSA key for decrypting @@ -176,7 +187,7 @@ final class RsaOaepPrivateKey { Hash hash, ) async { final impl = await webCryptImpl.rsaOaepPrivateKey.importJsonWebKey( - jwk, + _normalizeLegacyRsaOaepJwkAlg(jwk, hash), hash._impl, ); return RsaOaepPrivateKey._(impl); @@ -468,7 +479,7 @@ final class RsaOaepPublicKey { Hash hash, ) async { final impl = await webCryptImpl.rsaOaepPublicKey.importJsonWebKey( - jwk, + _normalizeLegacyRsaOaepJwkAlg(jwk, hash), hash._impl, ); return RsaOaepPublicKey._(impl);