From d79c9d7ef5412c3224db783bc014a16abd1107f1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 16 Oct 2021 14:02:06 +0100 Subject: [PATCH] cryptographic primitives (#118) * AES-GSM encryption with padding * RSA-OAEP encryption and key generation * SPKI encoding/decoding RSA public keys * rename functions * encode/decode RSA keys using asn1lib library * remove poitycastle namespace * remove unnecessary typecheck * fix: ci Co-authored-by: alex --- .../simplexmq/example/simplexmq_example.dart | 5 - packages/simplexmq/lib/src/crypto.dart | 93 +++++++++++++++ packages/simplexmq/lib/src/rsa_keys.dart | 109 ++++++++++++++++++ packages/simplexmq/pubspec.yaml | 6 +- packages/simplexmq/test/crypto_test.dart | 29 +++++ packages/simplexmq/test/rsa_keys_test.dart | 23 ++++ 6 files changed, 257 insertions(+), 8 deletions(-) create mode 100644 packages/simplexmq/lib/src/crypto.dart create mode 100644 packages/simplexmq/lib/src/rsa_keys.dart create mode 100644 packages/simplexmq/test/crypto_test.dart create mode 100644 packages/simplexmq/test/rsa_keys_test.dart diff --git a/packages/simplexmq/example/simplexmq_example.dart b/packages/simplexmq/example/simplexmq_example.dart index 45adcec72..8b1378917 100644 --- a/packages/simplexmq/example/simplexmq_example.dart +++ b/packages/simplexmq/example/simplexmq_example.dart @@ -1,6 +1 @@ -// import 'package:simplexmq/simplexmq.dart'; -// void main() { -// var awesome = Awesome(); -// print('awesome: ${awesome.isAwesome}'); -// } diff --git a/packages/simplexmq/lib/src/crypto.dart b/packages/simplexmq/lib/src/crypto.dart new file mode 100644 index 000000000..9cd168487 --- /dev/null +++ b/packages/simplexmq/lib/src/crypto.dart @@ -0,0 +1,93 @@ +import 'dart:math' show Random; +import 'dart:typed_data'; +import 'package:pointycastle/api.dart'; +import 'package:pointycastle/asymmetric/api.dart'; +import 'package:pointycastle/asymmetric/oaep.dart'; +import 'package:pointycastle/asymmetric/rsa.dart'; +import 'package:pointycastle/block/aes_fast.dart'; +import 'package:pointycastle/block/modes/gcm.dart'; +import 'package:pointycastle/key_generators/api.dart'; +import 'package:pointycastle/key_generators/rsa_key_generator.dart'; +import 'package:pointycastle/random/fortuna_random.dart'; + +class AESKey { + final Uint8List _key; + AESKey._make(this._key); + + static AESKey random([bool secure = false]) => + AESKey._make((secure ? secureRandomBytes : pseudoRandomBytes)(32)); + + static AESKey decode(Uint8List rawKey) => AESKey._make(rawKey); + + Uint8List encode() => _key; +} + +Uint8List randomIV() { + return pseudoRandomBytes(16); +} + +Uint8List secureRandomBytes(int len) { + return _randomBytes(len, Random.secure()); +} + +final sessionSeed = Random.secure(); + +Uint8List pseudoRandomBytes(int len) { + return _randomBytes(len, sessionSeed); +} + +// len should be divisible by 4 +Uint8List _randomBytes(int len, Random seedSource) { + final bytes = Uint8List(len); + for (int i = 0; i < len; i++) { + bytes[i] = seedSource.nextInt(256); + } + return bytes; +} + +final empty = Uint8List(0); +final paddingByte = '#'.codeUnitAt(0); + +Uint8List encryptAES(AESKey key, Uint8List iv, int padTo, Uint8List data) { + if (data.length >= padTo) throw ArgumentError('large message'); + final padded = Uint8List(padTo); + padded.setAll(0, data); + padded.fillRange(data.length, padTo, paddingByte); + return _makeGCMCipher(key, iv, true).process(padded); +} + +Uint8List decryptAES(AESKey key, Uint8List iv, Uint8List encryptedAndTag) { + return _makeGCMCipher(key, iv, false).process(encryptedAndTag); +} + +GCMBlockCipher _makeGCMCipher(AESKey key, Uint8List iv, bool encrypt) { + return GCMBlockCipher(AESFastEngine()) + ..init(encrypt, AEADParameters(KeyParameter(key._key), 128, iv, empty)); +} + +FortunaRandom _secureFortunaRandom() { + return FortunaRandom()..seed(KeyParameter(secureRandomBytes(32))); +} + +AsymmetricKeyPair generateRSAkeyPair( + [int bitLength = 2048]) { + final keyGen = RSAKeyGenerator() + ..init(ParametersWithRandom( + RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64), + _secureFortunaRandom())); + final pair = keyGen.generateKeyPair(); + return AsymmetricKeyPair( + pair.publicKey as RSAPublicKey, pair.privateKey as RSAPrivateKey); +} + +Uint8List encryptOAEP(RSAPublicKey key, Uint8List data) { + final oaep = OAEPEncoding(RSAEngine()) + ..init(true, PublicKeyParameter(key)); + return oaep.process(data); +} + +Uint8List decryptOAEP(RSAPrivateKey key, Uint8List data) { + final oaep = OAEPEncoding(RSAEngine()) + ..init(false, PrivateKeyParameter(key)); + return oaep.process(data); +} diff --git a/packages/simplexmq/lib/src/rsa_keys.dart b/packages/simplexmq/lib/src/rsa_keys.dart new file mode 100644 index 000000000..c2de0b999 --- /dev/null +++ b/packages/simplexmq/lib/src/rsa_keys.dart @@ -0,0 +1,109 @@ +import 'dart:typed_data'; +import 'package:asn1lib/asn1lib.dart'; +import 'package:pointycastle/asymmetric/api.dart' + show RSAPublicKey, RSAPrivateKey; + +const _rsaOid = '1.2.840.113549.1.1.1'; +final _asnRsaOid = ASN1ObjectIdentifier.fromComponentString(_rsaOid); +final _asn1Null = ASN1Null(); + +ASN1Sequence _asn1Sequence(List elements) { + final seq = ASN1Sequence()..elements = elements; + return seq; +} + +void _assertRsaAlgorithm(ASN1Sequence seq) { + if (seq.elements.isEmpty || + seq.elements[0] is! ASN1ObjectIdentifier || + (seq.elements[0] as ASN1ObjectIdentifier).identifier != _rsaOid) { + throw Exception('Invalid key algorithm identifier'); + } +} + +/// Decodes binary PKCS1 to [RSAPublicKey] +RSAPublicKey decodeRsaPubKeyPKCS1(Uint8List bytes) { + final els = ASN1Sequence.fromBytes(bytes).elements; + if (els.length != 2 || els[0] is! ASN1Integer || els[1] is! ASN1Integer) { + throw Exception('Invalid PKCS1 encoding'); + } + return RSAPublicKey( + (els[0] as ASN1Integer).valueAsBigInteger!, + (els[1] as ASN1Integer).valueAsBigInteger!, + ); +} + +/// Decodes binary SPKI to [RSAPublicKey] +RSAPublicKey decodeRsaPubKey(Uint8List bytes) { + final els = ASN1Sequence.fromBytes(bytes).elements; + if (els.length != 2 || els[1] is! ASN1BitString || els[0] is! ASN1Sequence) { + throw Exception('Invalid SPKI structure'); + } + _assertRsaAlgorithm(els[0] as ASN1Sequence); + return decodeRsaPubKeyPKCS1(els[1].valueBytes().sublist(1)); +} + +/// Encodes [key] as binary PKCS1 +Uint8List encodeRsaPubKeyPKCS1(RSAPublicKey key) => + _asn1Sequence([ASN1Integer(key.modulus!), ASN1Integer(key.publicExponent!)]) + .encodedBytes; + +/// Encodes [key] as binary SPKI +Uint8List encodeRsaPubKey(RSAPublicKey key) => _asn1Sequence([ + _asn1Sequence([_asnRsaOid, _asn1Null]), + ASN1BitString(encodeRsaPubKeyPKCS1(key)) + ]).encodedBytes; + +/// Decodes binary PKCS1 to [RSAPrivateKey] +RSAPrivateKey decodeRsaPrivKeyPKCS1(Uint8List bytes) { + final els = ASN1Sequence.fromBytes(bytes).elements; + if (els.length != 9 || els.any((el) => el is! ASN1Integer)) { + throw Exception('Invalid PKCS1 encoding'); + } + return RSAPrivateKey( + (els[1] as ASN1Integer).valueAsBigInteger!, + (els[3] as ASN1Integer).valueAsBigInteger!, + (els[4] as ASN1Integer).valueAsBigInteger!, + (els[5] as ASN1Integer).valueAsBigInteger!); +} + +/// Decodes binary PKCS8 to [RSAPrivateKey] +RSAPrivateKey decodeRsaPrivKey(Uint8List bytes) { + final els = ASN1Sequence.fromBytes(bytes).elements; + if (els.length != 3 || + els[1] is! ASN1Sequence || + els[2] is! ASN1OctetString) { + throw Exception('Invalid PKCS8 structure'); + } + _assertRsaAlgorithm(els[1] as ASN1Sequence); + return decodeRsaPrivKeyPKCS1(els[2].valueBytes()); +} + +final _asnZero = ASN1Integer(BigInt.from(0)); + +/// Encodes [key] as PKCS1 binary +Uint8List encodeRsaPrivKeyPKCS1(RSAPrivateKey key) { + final d = key.privateExponent!; + final p = key.p!; + final q = key.q!; + final dModP = d % (p - BigInt.from(1)); + final dModQ = d % (q - BigInt.from(1)); + final coefficient = q.modInverse(p); + return _asn1Sequence([ + _asnZero, + ASN1Integer(key.modulus!), + ASN1Integer(key.publicExponent!), + ASN1Integer(d), + ASN1Integer(p), + ASN1Integer(q), + ASN1Integer(dModP), + ASN1Integer(dModQ), + ASN1Integer(coefficient), + ]).encodedBytes; +} + +/// Encodes [key] as PKCS8 binary +Uint8List encodeRsaPrivKey(RSAPrivateKey key) => _asn1Sequence([ + _asnZero, + _asn1Sequence([_asnRsaOid, _asn1Null]), + ASN1OctetString(encodeRsaPrivKeyPKCS1(key)) + ]).encodedBytes; diff --git a/packages/simplexmq/pubspec.yaml b/packages/simplexmq/pubspec.yaml index ea5669f5f..5b90e7c3f 100644 --- a/packages/simplexmq/pubspec.yaml +++ b/packages/simplexmq/pubspec.yaml @@ -6,9 +6,9 @@ publish_to: none environment: sdk: '>=2.14.0 <3.0.0' - -# dependencies: -# path: ^1.8.0 +dependencies: + asn1lib: ^1.0.2 + pointycastle: ^3.3.4 dev_dependencies: lints: ^1.0.0 diff --git a/packages/simplexmq/test/crypto_test.dart b/packages/simplexmq/test/crypto_test.dart new file mode 100644 index 000000000..2482e895c --- /dev/null +++ b/packages/simplexmq/test/crypto_test.dart @@ -0,0 +1,29 @@ +import 'package:simplexmq/src/buffer.dart'; +import 'package:simplexmq/src/crypto.dart'; +import 'package:test/test.dart'; + +void main() { + group('AES-GCM encryption with padding', () { + test('encrypt and decrypt', () { + final key = AESKey.random(); + final iv = pseudoRandomBytes(16); + final data = encodeAscii('hello'); + final cipherText = encryptAES(key, iv, 32, data); + expect(cipherText.length, 32 + 16); + final decrypted = decryptAES(key, iv, cipherText); + expect(decodeAscii(decrypted), + 'hello' + List.filled(32 - 'hello'.length, '#').join()); + }); + }); + + group('RSA-OAEP encryption', () { + test('encrypt and decrypt', () { + final keyPair = generateRSAkeyPair(); + final data = encodeAscii('hello there'); + final cipherText = encryptOAEP(keyPair.publicKey, data); + expect(cipherText.length, 2048 ~/ 8); + final decrypted = decryptOAEP(keyPair.privateKey, cipherText); + expect(decodeAscii(decrypted), 'hello there'); + }); + }); +} diff --git a/packages/simplexmq/test/rsa_keys_test.dart b/packages/simplexmq/test/rsa_keys_test.dart new file mode 100644 index 000000000..ad1d5b372 --- /dev/null +++ b/packages/simplexmq/test/rsa_keys_test.dart @@ -0,0 +1,23 @@ +import 'package:simplexmq/src/buffer.dart'; +import 'package:simplexmq/src/rsa_keys.dart'; +import 'package:test/test.dart'; + +void main() { + group('RSA keys encoding', () { + test('SPKI encode/decode RSA public key', () { + final keyStr = + 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fdE2lndTnPi7QHS2OqP1YE6ZH6sGdf7Boji6jPgQVJB289aQAPZRSJlg6s+xHC52sa2isFiuZN2uFENNWznuZsOWXkHMthbo9Qkp7ZjOhomZURtsIsaRny9GTcaFOrd19rqbsrCRLyb3xtwbQjv/2HEGNZyP9YsGsZijTJaV0yQNEp/5Gt3jHebJ8mqLdBr/aDQBf3oSsmUDDvocGU4kL14GOuVYCKNlEUrFe1X1poSXLH0uu485GVfHB72XjKP/flS2rL91fguqMil1nkelL1K4WOyx1Z87LyyXT2Vh4GRLVHG/a9LyPpw7ovQlO5RIr6suODkXwbAUHq/8j5IDwIDAQAB'; + final key = decodeRsaPubKey(decode64(encodeAscii(keyStr))!); + final encKey = decodeAscii(encode64(encodeRsaPubKey(key))); + expect(encKey, keyStr); + }); + + test('PKCS8 encode/decode RSA private key', () { + final keyStr = + 'MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDZ90TaWd1Oc+LtAdLY6o/VgTpkfqwZ1/sGiOLqM+BBUkHbz1pAA9lFImWDqz7EcLnaxraKwWK5k3a4UQ01bOe5mw5ZeQcy2Fuj1CSntmM6GiZlRG2wixpGfL0ZNxoU6t3X2upuysJEvJvfG3BtCO//YcQY1nI/1iwaxmKNMlpXTJA0Sn/ka3eMd5snyaot0Gv9oNAF/ehKyZQMO+hwZTiQvXgY65VgIo2URSsV7VfWmhJcsfS67jzkZV8cHvZeMo/9+VLasv3V+C6oyKXWeR6UvUrhY7LHVnzsvLJdPZWHgZEtUcb9r0vI+nDui9CU7lEivqy44ORfBsBQer/yPkgPAgMBAAECggEAezFPgB4EgB/tpUk/k4xXiTPF/iC+QskYvyPFJNv3JtRIFuWGO+Iw/esn9xhlnH+d+/IOIDSXCQ44ropY7dZEzlm97YIDOJCikuEHaqciRCedheT8Hikwy6Aa/NJw8lug0SyRDdeZn2H+s0X98BJ6Gxx1yhgCcOQq/2MbNnS8LNQ0yNNHu9Ds2K5Weiwhb9nrLLuMvrF/k1z0QNi5mCzDZK8iDMr87UZycmKKue13/xppI1pddJm4Ta13/OmZtYe2d5UgK9FrLStFkl7yqWnIcDueCOZvqo4nIfxPlPVolQ9B9RXL2tctkYRVy6FBkZIJkSk4O1Vz5BuPgBy9McoRMQKBgQD5xmVKAnvBQ08BUGXw5HVy4qR1Oj/EmxXKNypZsTdaKpiMfjNB8GbegO0na9Ry7sRo5g15vXpjCS6LDwGx4fZ/5u4N5CP3845DscrZjebs0tS7u5USPMoMzZ/KYfddRsGdm7y9HMp3Z6O3ZGGqZ15VFHGYjRjzK8BYE91rvv1WlwKBgQDfZe0c8/aa25KHW2zLRtSOR8ze+pz79hxNmpCeOoyzfaVRzwDh3KUl78PEWqMbJ7EdEdokCisFU0yEpuuc29JD6l9YuQmYH2VGdyg52iPPCXJOP1PBO0VQ/D/cAmZd/75cmoiyC7kELHfiAWBqO/7xpWkNiEpZZcI33DbzReYBSQKBgBKGuaqUppM+J9UEHpuQhnmf/+zGBkbR7frSvqxqbZ2dfTUmgyzH5Qlp7K043UgtF5pkPemiuToxSyd7VHfaN8ti2JNlMZnJkerJfC9IzDESrj7CehshMSdj9Q8w1wUvI1tKWuR4Bzh2Enme03OtORz8aDSVep1GyHx/9LNyNh4/AoGAcpdk/nIB8ENrMTVrZAYsJ+OaqlIhTnla4U/EmPVtkPCFaaZmTHUS3ZfUcpcPjXFZv5CVteDlWnD1EiJRP3/epmnFiMw5qKeKGpAquSo1LhEpagu/2aGel8EcvK0ad2Mk8XlvXuz2dbads/eCzluCFdAESCW+BYdWDbNPGJClP8kCgYBoT+0res0efi1cn6H0fPx/q33Wmgf47txVrzQN0ZEWDFOOhnErvGpRan9AG+LGvp7TvWWHnW13qjFCXGocWcbaoqsLabkov961R8ij2MTeToz6V+7YfK0KBt/h2HHJ5t/CybNxE5iYFyUMI7GTlC2GzFrnvxH/UYwUma1AplkpEw=='; + final key = decodeRsaPrivKey(decode64(encodeAscii(keyStr))!); + final encKey = decodeAscii(encode64(encodeRsaPrivKey(key))); + expect(encKey, keyStr); + }); + }); +}