| // See file LICENSE for more information. |
| |
| library impl.signer.rsa_signer; |
| |
| import 'dart:typed_data'; |
| |
| import 'package:pointycastle/api.dart'; |
| import 'package:pointycastle/asymmetric/api.dart'; |
| import 'package:pointycastle/asymmetric/pkcs1.dart'; |
| import 'package:pointycastle/asymmetric/rsa.dart'; |
| import 'package:pointycastle/src/registry/registry.dart'; |
| |
| // TODO: implement full ASN1 encoding (for now I will do a little ad-hoc implementation of just what is needed here) |
| class RSASigner implements Signer { |
| /// Intended for internal use. |
| static final FactoryConfig factoryConfig = |
| DynamicFactoryConfig.suffix(Signer, '/RSA', (_, Match match) { |
| final digestName = match.group(1); |
| final digestIdentifierHex = _digestIdentifierHexes[digestName!]; |
| if (digestIdentifierHex == null) { |
| throw RegistryFactoryException( |
| 'RSA signing with digest $digestName is not supported'); |
| } |
| return () => RSASigner(Digest(digestName), digestIdentifierHex); |
| }); |
| |
| static final Map<String, String> _digestIdentifierHexes = { |
| 'MD2': '06082a864886f70d0202', |
| 'MD4': '06082a864886f70d0204', |
| 'MD5': '06082a864886f70d0205', |
| 'RIPEMD-128': '06052b24030202', |
| 'RIPEMD-160': '06052b24030201', |
| 'RIPEMD-256': '06052b24030203', |
| 'SHA-1': '06052b0e03021a', |
| 'SHA-224': '0609608648016503040204', |
| 'SHA-256': '0609608648016503040201', |
| 'SHA-384': '0609608648016503040202', |
| 'SHA-512': '0609608648016503040203' |
| }; |
| |
| final AsymmetricBlockCipher _rsa = PKCS1Encoding(RSAEngine()); |
| final Digest _digest; |
| late Uint8List |
| _digestIdentifier; // DER encoded with trailing tag (06)+length byte |
| late bool _forSigning; |
| |
| RSASigner(this._digest, String digestIdentifierHex) { |
| _digestIdentifier = _hexStringToBytes(digestIdentifierHex); |
| } |
| |
| @override |
| String get algorithmName => '${_digest.algorithmName}/RSA'; |
| |
| @override |
| void reset() { |
| _digest.reset(); |
| _rsa.reset(); |
| } |
| |
| @override |
| void init(bool forSigning, CipherParameters params) { |
| _forSigning = forSigning; |
| |
| AsymmetricKeyParameter akparams; |
| if (params is ParametersWithRandom) { |
| akparams = params.parameters as AsymmetricKeyParameter<AsymmetricKey>; |
| } else { |
| akparams = params as AsymmetricKeyParameter<AsymmetricKey>; |
| } |
| var k = akparams.key as RSAAsymmetricKey; |
| |
| if (forSigning && (k is! RSAPrivateKey)) { |
| throw ArgumentError('Signing requires private key'); |
| } |
| |
| if (!forSigning && (k is! RSAPublicKey)) { |
| throw ArgumentError('Verification requires public key'); |
| } |
| |
| reset(); |
| |
| _rsa.init(forSigning, params); |
| } |
| |
| @override |
| RSASignature generateSignature(Uint8List message, {bool normalize = false}) { |
| if (!_forSigning) { |
| throw StateError('Signer was not initialised for signature generation'); |
| } |
| |
| var hash = Uint8List(_digest.digestSize); |
| _digest.reset(); |
| _digest.update(message, 0, message.length); |
| _digest.doFinal(hash, 0); |
| |
| var data = _derEncode(hash); |
| var out = Uint8List(_rsa.outputBlockSize); |
| var len = _rsa.processBlock(data, 0, data.length, out, 0); |
| return RSASignature(out.sublist(0, len)); |
| } |
| |
| @override |
| bool verifySignature(Uint8List message, covariant RSASignature signature) { |
| if (_forSigning) { |
| throw StateError('Signer was not initialised for signature verification'); |
| } |
| |
| var hash = Uint8List(_digest.digestSize); |
| _digest.reset(); |
| _digest.update(message, 0, message.length); |
| _digest.doFinal(hash, 0); |
| var sig = Uint8List(_rsa.outputBlockSize); |
| |
| try { |
| final len = |
| _rsa.processBlock(signature.bytes, 0, signature.bytes.length, sig, 0); |
| sig = sig.sublist(0, len); |
| } on ArgumentError { |
| // Signature was tampered with so the RSA 'decrypted' block is totally |
| // different to the original, causing [PKCS1Encoding._decodeBlock] to |
| // throw an exception because it does not recognise it. |
| return false; |
| } |
| |
| var expected = _derEncode(hash); |
| |
| if (sig.length == expected.length) { |
| for (var i = 0; i < sig.length; i++) { |
| if (sig[i] != expected[i]) { |
| return false; |
| } |
| } |
| return true; //return Arrays.constantTimeAreEqual(sig, expected); |
| } else if (sig.length == expected.length - 2) { |
| // NULL left out |
| var sigOffset = sig.length - hash.length - 2; |
| var expectedOffset = expected.length - hash.length - 2; |
| |
| expected[1] -= 2; // adjust lengths |
| expected[3] -= 2; |
| |
| var nonEqual = 0; |
| |
| for (var i = 0; i < hash.length; i++) { |
| nonEqual |= sig[sigOffset + i] ^ expected[expectedOffset + i]; |
| } |
| |
| for (var i = 0; i < sigOffset; i++) { |
| nonEqual |= sig[i] ^ expected[i]; // check header less NULL |
| } |
| |
| return nonEqual == 0; |
| } else { |
| return false; |
| } |
| } |
| |
| Uint8List _derEncode(Uint8List hash) { |
| var out = Uint8List(2 + 2 + _digestIdentifier.length + 2 + 2 + hash.length); |
| var i = 0; |
| |
| // header |
| out[i++] = 48; |
| out[i++] = out.length - 2; |
| |
| // algorithmIdentifier.header |
| out[i++] = 48; |
| out[i++] = _digestIdentifier.length + 2; |
| |
| // algorithmIdentifier.bytes |
| out.setAll(i, _digestIdentifier); |
| i += _digestIdentifier.length; |
| |
| // algorithmIdentifier.null |
| out[i++] = 5; |
| out[i++] = 0; |
| |
| // hash.header |
| out[i++] = 4; |
| out[i++] = hash.length; |
| |
| // hash.bytes |
| out.setAll(i, hash); |
| |
| return out; |
| } |
| |
| Uint8List _hexStringToBytes(String hex) { |
| var result = Uint8List(hex.length ~/ 2); |
| for (var i = 0; i < hex.length; i += 2) { |
| var num = hex.substring(i, i + 2); |
| var byte = int.parse(num, radix: 16); |
| result[i ~/ 2] = byte; |
| } |
| return result; |
| } |
| } |