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 <alex@tekartik.com>
This commit is contained in:
Evgeny Poberezkin 2021-10-16 14:02:06 +01:00 committed by GitHub
parent fd247a4e6b
commit d79c9d7ef5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 257 additions and 8 deletions

View File

@ -1,6 +1 @@
// import 'package:simplexmq/simplexmq.dart';
// void main() {
// var awesome = Awesome();
// print('awesome: ${awesome.isAwesome}');
// }

View File

@ -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<RSAPublicKey, RSAPrivateKey> generateRSAkeyPair(
[int bitLength = 2048]) {
final keyGen = RSAKeyGenerator()
..init(ParametersWithRandom(
RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64),
_secureFortunaRandom()));
final pair = keyGen.generateKeyPair();
return AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>(
pair.publicKey as RSAPublicKey, pair.privateKey as RSAPrivateKey);
}
Uint8List encryptOAEP(RSAPublicKey key, Uint8List data) {
final oaep = OAEPEncoding(RSAEngine())
..init(true, PublicKeyParameter<RSAPublicKey>(key));
return oaep.process(data);
}
Uint8List decryptOAEP(RSAPrivateKey key, Uint8List data) {
final oaep = OAEPEncoding(RSAEngine())
..init(false, PrivateKeyParameter<RSAPrivateKey>(key));
return oaep.process(data);
}

View File

@ -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<ASN1Object> 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;

View File

@ -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

View File

@ -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');
});
});
}

View File

@ -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);
});
});
}