| /// Demonstration of RSA |
| /// |
| /// - key generation |
| /// - signing and verification |
| /// - encrypting and decrypting |
| /// |
| /// Invoke with "-v" to print extra information. |
| /// Invoke with "-l" to use longer plaintext. |
| |
| import 'dart:convert'; |
| import 'dart:math'; |
| import 'dart:typed_data'; |
| |
| // For using the registry: |
| //import 'package:pointycastle/pointycastle.dart'; |
| |
| // When not using the registry: |
| import 'package:pointycastle/export.dart'; |
| |
| //================================================================ |
| // Test data |
| |
| const shortPlaintext = 'What hath God wrought!'; |
| |
| const longPlaintext = ''' |
| Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor |
| incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis |
| nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. |
| Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore |
| eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt |
| in culpa qui officia deserunt mollit anim id est laborum.'''; |
| |
| //================================================================ |
| // Key generation |
| |
| //---------------------------------------------------------------- |
| /// Generate an RSA key pair. |
| |
| AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey> generateRSAkeyPair( |
| SecureRandom secureRandom, |
| {int bitLength = 2048}) { |
| // Create an RSA key generator and initialize it |
| |
| // final keyGen = KeyGenerator('RSA'); // Get using registry |
| final keyGen = RSAKeyGenerator(); // Get directly |
| |
| keyGen.init(ParametersWithRandom( |
| RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64), |
| secureRandom)); |
| |
| // Use the generator |
| |
| final pair = keyGen.generateKeyPair(); |
| |
| // Examine the generated key-pair |
| |
| final myPublic = pair.publicKey as RSAPublicKey; |
| final myPrivate = pair.privateKey as RSAPrivateKey; |
| |
| // The RSA numbers will always satisfy these properties |
| |
| assert(myPublic.modulus == myPrivate.modulus); |
| assert(myPrivate.p! * myPrivate.q! == myPrivate.modulus, 'p.q != n'); |
| final phi = (myPrivate.p! - BigInt.one) * (myPrivate.q! - BigInt.one); |
| assert((myPublic.exponent! * myPrivate.exponent!) % phi == BigInt.one); |
| |
| return AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>(myPublic, myPrivate); |
| } |
| |
| //================================================================ |
| // Signing and verifying |
| |
| //---------------------------------------------------------------- |
| /// Use an RSA private key to create a signature. |
| |
| Uint8List rsaSign(RSAPrivateKey privateKey, Uint8List dataToSign) { |
| //final signer = Signer('SHA-256/RSA'); // Get using registry |
| final signer = RSASigner(SHA256Digest(), '0609608648016503040201'); |
| |
| // '0609608648016503040201' is the BER encoding of the Object Identifier |
| // 2.16.840.1.101.3.4.2.1 that identifies the SHA-256 digest algorithm. |
| // <http://oid-info.com/get/2.16.840.1.101.3.4.2.1> |
| |
| // See _DIGEST_IDENTIFIER_HEXES in RSASigner for correct hex values to use |
| // IMPORTANT: the correct digest identifier hex value must be used, |
| // corresponding to the digest algorithm, otherwise the signature won't |
| // verify. |
| |
| signer.init(true, PrivateKeyParameter<RSAPrivateKey>(privateKey)); |
| |
| final sig = signer.generateSignature(dataToSign); |
| |
| return sig.bytes; |
| } |
| |
| //---------------------------------------------------------------- |
| /// Use an RSA public key to verify a signature. |
| |
| bool rsaVerify( |
| RSAPublicKey publicKey, Uint8List signedData, Uint8List signature) { |
| final sig = RSASignature(signature); |
| //final signer = Signer('SHA-256/RSA'); // Get using registry |
| final verifier = RSASigner(SHA256Digest(), '0609608648016503040201'); |
| // See _DIGEST_IDENTIFIER_HEXES in RSASigner for correct hex values to use |
| // IMPORTANT: the correct digest identifier hex value must be used, |
| // corresponding to the digest algorithm, otherwise the signature won't |
| // verify. |
| |
| verifier.init(false, PublicKeyParameter<RSAPublicKey>(publicKey)); |
| |
| return verifier.verifySignature(signedData, sig); |
| } |
| |
| //================================================================ |
| // Encryption and decryption |
| |
| //---------------------------------------------------------------- |
| /// Schemes to use to encrypt/decrypt |
| /// |
| /// rsa = use RSAEngine without an Asymmetric Block Cipher. |
| |
| enum AsymBlockCipherToUse { rsa, pkcs1, oaep } |
| |
| //---------------------------------------------------------------- |
| |
| AsymmetricBlockCipher _createBlockCipher(AsymBlockCipherToUse scheme) { |
| switch (scheme) { |
| case AsymBlockCipherToUse.rsa: |
| return RSAEngine(); |
| case AsymBlockCipherToUse.pkcs1: |
| return PKCS1Encoding(RSAEngine()); |
| case AsymBlockCipherToUse.oaep: |
| return OAEPEncoding(RSAEngine()); |
| } |
| } |
| |
| Uint8List rsaEncrypt(RSAPublicKey myPublic, Uint8List dataToEncrypt, |
| AsymBlockCipherToUse scheme) { |
| var encryptor = _createBlockCipher(scheme); |
| |
| encryptor.init( |
| true, |
| PublicKeyParameter<RSAPublicKey>(myPublic), |
| ); // true=encrypt |
| |
| return _processInBlocks(encryptor, dataToEncrypt); |
| } |
| |
| //---------------------------------------------------------------- |
| |
| Uint8List rsaDecrypt(RSAPrivateKey myPrivate, Uint8List cipherText, |
| AsymBlockCipherToUse scheme) { |
| var decryptor = _createBlockCipher(scheme); |
| |
| decryptor.init( |
| false, |
| PrivateKeyParameter<RSAPrivateKey>(myPrivate), |
| ); // false=decrypt |
| |
| return _processInBlocks(decryptor, cipherText); |
| } |
| |
| //---------------------------------------------------------------- |
| |
| Uint8List _processInBlocks(AsymmetricBlockCipher engine, Uint8List input) { |
| final numBlocks = input.length ~/ engine.inputBlockSize + |
| ((input.length % engine.inputBlockSize != 0) ? 1 : 0); |
| |
| final output = Uint8List(numBlocks * engine.outputBlockSize); |
| |
| var inputOffset = 0; |
| var outputOffset = 0; |
| while (inputOffset < input.length) { |
| final chunkSize = (inputOffset + engine.inputBlockSize <= input.length) |
| ? engine.inputBlockSize |
| : input.length - inputOffset; |
| |
| outputOffset += engine.processBlock( |
| input, inputOffset, chunkSize, output, outputOffset); |
| |
| inputOffset += chunkSize; |
| } |
| |
| return (output.length == outputOffset) |
| ? output |
| : output.sublist(0, outputOffset); |
| } |
| |
| //================================================================ |
| // Supporting functions |
| // |
| // These are not a part of RSA, so different implementations may do these |
| // things differently. |
| |
| //---------------------------------------------------------------- |
| |
| SecureRandom getSecureRandom() { |
| // Create a secure random number generator and seed it with random bytes |
| |
| //final result = SecureRandom('Fortuna'); // Get using registry |
| final secureRandom = FortunaRandom(); // Get directly |
| |
| final seedSource = Random.secure(); |
| final seeds = <int>[]; |
| for (var i = 0; i < 32; i++) { |
| seeds.add(seedSource.nextInt(255)); |
| } |
| secureRandom.seed(KeyParameter(Uint8List.fromList(seeds))); |
| |
| return secureRandom; |
| } |
| |
| //---------------------------------------------------------------- |
| // Modify one bit. |
| // |
| // Returns a new Uint8List with the modified bytes. |
| |
| Uint8List tamperWithData(Uint8List original) { |
| // Tampered with data does not verify |
| |
| final tamperedData = Uint8List.fromList(original); |
| tamperedData[tamperedData.length - 1] ^= 0x01; // XOR to flip one bit |
| |
| return tamperedData; |
| } |
| |
| //---------------------------------------------------------------- |
| // Print out the RSA key pair |
| |
| String dumpRsaKeys(AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey> k, |
| {bool verbose = false}) { |
| final bitLength = k.privateKey.modulus!.bitLength; |
| final buf = StringBuffer('RSA key generated (bit-length: $bitLength)'); |
| |
| if (verbose) { |
| buf.write(''' |
| e = ${k.publicKey.exponent} |
| n = ${k.publicKey.modulus} |
| Private: |
| n = ${k.privateKey.modulus} |
| d = ${k.privateKey.exponent} |
| p = ${k.privateKey.p} |
| q = ${k.privateKey.q} |
| '''); |
| } |
| return buf.toString(); |
| } |
| |
| //---------------------------------------------------------------- |
| /// Represent bytes in hexadecimal |
| /// |
| /// If a [separator] is provided, it is placed the hexadecimal characters |
| /// representing each byte. Otherwise, all the hexadecimal characters are |
| /// simply concatenated together. |
| |
| String bin2hex(Uint8List bytes, {String? separator, int? wrap}) { |
| var len = 0; |
| final buf = StringBuffer(); |
| for (final b in bytes) { |
| final s = b.toRadixString(16); |
| if (buf.isNotEmpty && separator != null) { |
| buf.write(separator); |
| len += separator.length; |
| } |
| |
| if (wrap != null && wrap < len + 2) { |
| buf.write('\n'); |
| len = 0; |
| } |
| |
| buf.write('${(s.length == 1) ? '0' : ''}$s'); |
| len += 2; |
| } |
| return buf.toString(); |
| } |
| |
| //---------------------------------------------------------------- |
| // Decode a hexadecimal string into a sequence of bytes. |
| |
| Uint8List hex2bin(String hexStr) { |
| if (hexStr.length % 2 != 0) { |
| throw const FormatException('not an even number of hexadecimal characters'); |
| } |
| final result = Uint8List(hexStr.length ~/ 2); |
| for (var i = 0; i < result.length; i++) { |
| result[i] = int.parse(hexStr.substring(2 * i, 2 * (i + 1)), radix: 16); |
| } |
| return result; |
| } |
| |
| //---------------------------------------------------------------- |
| /// Tests two Uint8List for equality. |
| /// |
| /// Returns true if they contain all the same bytes. Otherwise false. |
| |
| bool isUint8ListEqual(Uint8List a, Uint8List b) { |
| if (a.length == b.length) { |
| for (var x = 0; x < a.length; x++) { |
| if (a[x] != b[x]) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| //================================================================ |
| |
| void _testSignAndVerify(AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey> rsaPair, |
| Uint8List bytesToSign, bool verbose) { |
| final signatureBytes = rsaSign(rsaPair.privateKey, bytesToSign); |
| if (verbose) { |
| print('Signature:\n${bin2hex(signatureBytes, wrap: 64)}'); |
| } |
| |
| if (rsaVerify(rsaPair.publicKey, bytesToSign, signatureBytes)) { |
| print('Signature verify: success'); |
| } else { |
| print('fail: signature did not verify'); |
| } |
| if (rsaVerify( |
| rsaPair.publicKey, tamperWithData(bytesToSign), signatureBytes)) { |
| print('fail: signature verifies when data was modified'); |
| } else { |
| print('Signature verify: detected tampered text successfully'); |
| } |
| |
| try { |
| if (rsaVerify( |
| rsaPair.publicKey, bytesToSign, tamperWithData(signatureBytes))) { |
| print('fail: signature verifies when signature was modified'); |
| } else { |
| print('Signature verify: detected tampered signature successfully'); |
| } |
| } catch (e, st) { |
| print('fail: signature validation: threw exception: ${e.runtimeType}'); |
| if (verbose) { |
| print('$e\n$st\n'); |
| } |
| } |
| } |
| |
| //---------------------------------------------------------------- |
| |
| void _testEncryptAndDecrypt( |
| AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey> rsaPair, |
| AsymBlockCipherToUse scheme, |
| Uint8List plaintext, |
| bool verbose) { |
| try { |
| if (verbose) { |
| print('\nEncrypting with $scheme:'); |
| } |
| final cipherText = rsaEncrypt(rsaPair.publicKey, plaintext, scheme); |
| if (verbose) { |
| //print('\nPlaintext:\n"$plaintext"'); |
| print('Ciphertext:\n${bin2hex(cipherText, wrap: 64)}'); |
| } |
| |
| final decryptedBytes = rsaDecrypt(rsaPair.privateKey, cipherText, scheme); |
| |
| if (isUint8ListEqual(decryptedBytes, plaintext)) { |
| if (verbose) { |
| print('Decrypted:\n"${utf8.decode(decryptedBytes)}"'); |
| } |
| print('Decrypt ($scheme): success'); |
| } else { |
| print(plaintext); |
| print(decryptedBytes); |
| print( |
| 'Decrypted:\n"${utf8.decode(decryptedBytes, allowMalformed: true)}"'); |
| print('fail: decrypted does not match plaintext'); |
| } |
| } catch (e, st) { |
| print('fail: threw unexpected exception: ${e.runtimeType}'); |
| if (verbose) { |
| print('$e\n$st\n'); |
| } |
| } |
| } |
| //---------------------------------------------------------------- |
| |
| void main(List<String> args) { |
| var longText = false; |
| var verbose = false; |
| for (final arg in args) { |
| switch (arg) { |
| case '--long': |
| case '-l': |
| longText = true; |
| break; |
| case '--help': |
| case '-h': |
| print('Usage: rsa-demo [-l] [-v] [-h]'); |
| return; |
| case '--verbose': |
| case '-v': |
| verbose = true; |
| break; |
| default: |
| print('Usage error: unknown argument: $arg (-h for help)'); |
| return; |
| } |
| } |
| |
| // Generate an RSA key pair |
| |
| final rsaPair = generateRSAkeyPair(getSecureRandom(), bitLength: 1024); |
| print(dumpRsaKeys(rsaPair, verbose: verbose)); |
| |
| // Use the key pair |
| |
| final plaintext = (longText) ? longPlaintext : shortPlaintext; |
| if (verbose) { |
| print('Plaintext: $plaintext\n'); |
| } |
| final bytes = utf8.encode(plaintext); |
| |
| _testSignAndVerify(rsaPair, Uint8List.fromList(bytes), verbose); |
| |
| _testEncryptAndDecrypt( |
| rsaPair, AsymBlockCipherToUse.rsa, Uint8List.fromList(bytes), verbose); |
| _testEncryptAndDecrypt( |
| rsaPair, AsymBlockCipherToUse.pkcs1, Uint8List.fromList(bytes), verbose); |
| _testEncryptAndDecrypt( |
| rsaPair, AsymBlockCipherToUse.oaep, Uint8List.fromList(bytes), verbose); |
| } |