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:
parent
fd247a4e6b
commit
d79c9d7ef5
@ -1,6 +1 @@
|
||||
// import 'package:simplexmq/simplexmq.dart';
|
||||
|
||||
// void main() {
|
||||
// var awesome = Awesome();
|
||||
// print('awesome: ${awesome.isAwesome}');
|
||||
// }
|
||||
|
93
packages/simplexmq/lib/src/crypto.dart
Normal file
93
packages/simplexmq/lib/src/crypto.dart
Normal 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);
|
||||
}
|
109
packages/simplexmq/lib/src/rsa_keys.dart
Normal file
109
packages/simplexmq/lib/src/rsa_keys.dart
Normal 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;
|
@ -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
|
||||
|
29
packages/simplexmq/test/crypto_test.dart
Normal file
29
packages/simplexmq/test/crypto_test.dart
Normal 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');
|
||||
});
|
||||
});
|
||||
}
|
23
packages/simplexmq/test/rsa_keys_test.dart
Normal file
23
packages/simplexmq/test/rsa_keys_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user