Browse Source

make a few methods async to move argon2 into separate isolate.

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
8041b1e83d
  1. 2
      bin/kdbx.dart
  2. 1
      lib/kdbx.dart
  3. 67
      lib/src/crypto/argon2.dart
  4. 11
      lib/src/crypto/key_encrypter_kdf.dart
  5. 26
      lib/src/kdbx_format.dart
  6. 41
      test/kdbx4_test.dart
  7. 14
      test/kdbx_test.dart

2
bin/kdbx.dart

@ -74,7 +74,7 @@ abstract class KdbxFileCommand extends Command<void> {
final bytes = await File(inputFile).readAsBytes(); final bytes = await File(inputFile).readAsBytes();
final password = prompts.get('Password for $inputFile', final password = prompts.get('Password for $inputFile',
conceal: true, validate: (str) => str.isNotEmpty); conceal: true, validate: (str) => str.isNotEmpty);
final file = KdbxFormat(null) final file = await KdbxFormat(null)
.read(bytes, Credentials(ProtectedValue.fromString(password))); .read(bytes, Credentials(ProtectedValue.fromString(password)));
return runWithFile(file); return runWithFile(file);
} }

1
lib/kdbx.dart

@ -2,7 +2,6 @@
library kdbx; library kdbx;
export 'src/crypto/argon2.dart'; export 'src/crypto/argon2.dart';
export 'src/crypto/key_encrypter_kdf.dart' show Argon2;
export 'src/crypto/protected_value.dart' export 'src/crypto/protected_value.dart'
show ProtectedValue, StringValue, PlainValue; show ProtectedValue, StringValue, PlainValue;
export 'src/kdbx_consts.dart'; export 'src/kdbx_consts.dart';

67
lib/src/crypto/argon2.dart

@ -6,6 +6,8 @@ import 'package:ffi/ffi.dart';
import 'package:ffi_helper/ffi_helper.dart'; import 'package:ffi_helper/ffi_helper.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
// ignore_for_file: non_constant_identifier_names
typedef Argon2HashNative = Pointer<Utf8> Function( typedef Argon2HashNative = Pointer<Utf8> Function(
Pointer<Uint8> key, Pointer<Uint8> key,
IntPtr keyLen, IntPtr keyLen,
@ -33,16 +35,23 @@ typedef Argon2Hash = Pointer<Utf8> Function(
); );
abstract class Argon2 { abstract class Argon2 {
Uint8List argon2( Uint8List argon2(Argon2Arguments args);
Uint8List key,
Uint8List salt, Future<Uint8List> argon2Async(Argon2Arguments args);
int memory, }
int iterations,
int length, class Argon2Arguments {
int parallelism, Argon2Arguments(this.key, this.salt, this.memory, this.iterations,
int type, this.length, this.parallelism, this.type, this.version);
int version,
); final Uint8List key;
final Uint8List salt;
final int memory;
final int iterations;
final int length;
final int parallelism;
final int type;
final int version;
} }
abstract class Argon2Base extends Argon2 { abstract class Argon2Base extends Argon2 {
@ -50,21 +59,12 @@ abstract class Argon2Base extends Argon2 {
Argon2Hash get argon2hash; Argon2Hash get argon2hash;
@override @override
Uint8List argon2( Uint8List argon2(Argon2Arguments args) {
Uint8List key, final keyArray = Uint8Array.fromTypedList(args.key);
Uint8List salt,
int memory,
int iterations,
int length,
int parallelism,
int type,
int version,
) {
final keyArray = Uint8Array.fromTypedList(key);
// final saltArray = Uint8Array.fromTypedList(salt); // final saltArray = Uint8Array.fromTypedList(salt);
final saltArray = allocate<Uint8>(count: salt.length); final saltArray = allocate<Uint8>(count: args.salt.length);
final saltList = saltArray.asTypedList(length); final saltList = saltArray.asTypedList(args.length);
saltList.setAll(0, salt); saltList.setAll(0, args.salt);
// const memoryCost = 1 << 16; // const memoryCost = 1 << 16;
// _logger.fine('saltArray: ${ByteUtils.toHexList(saltArray.view)}'); // _logger.fine('saltArray: ${ByteUtils.toHexList(saltArray.view)}');
@ -73,13 +73,13 @@ abstract class Argon2Base extends Argon2 {
keyArray.rawPtr, keyArray.rawPtr,
keyArray.length, keyArray.length,
saltArray, saltArray,
salt.length, args.salt.length,
memory, args.memory,
iterations, args.iterations,
parallelism, args.parallelism,
length, args.length,
type, args.type,
version, args.version,
); );
keyArray.free(); keyArray.free();
@ -88,4 +88,9 @@ abstract class Argon2Base extends Argon2 {
final resultString = Utf8.fromUtf8(result); final resultString = Utf8.fromUtf8(result);
return base64.decode(resultString); return base64.decode(resultString);
} }
@override
Future<Uint8List> argon2Async(Argon2Arguments args) async {
return argon2(args);
}
} }

11
lib/src/crypto/key_encrypter_kdf.dart

@ -78,7 +78,7 @@ class KeyEncrypterKdf {
final Argon2 argon2; final Argon2 argon2;
Uint8List encrypt(Uint8List key, VarDictionary kdfParameters) { Future<Uint8List> encrypt(Uint8List key, VarDictionary kdfParameters) async {
final uuid = kdfParameters.get(ValueType.typeBytes, '\$UUID'); final uuid = kdfParameters.get(ValueType.typeBytes, '\$UUID');
if (uuid == null) { if (uuid == null) {
throw KdbxCorruptedFileException('No Kdf UUID'); throw KdbxCorruptedFileException('No Kdf UUID');
@ -87,7 +87,7 @@ class KeyEncrypterKdf {
switch (kdfUuids[kdfUuid]) { switch (kdfUuids[kdfUuid]) {
case KdfType.Argon2: case KdfType.Argon2:
_logger.fine('Must be using argon2'); _logger.fine('Must be using argon2');
return encryptArgon2(key, kdfParameters); return await encryptArgon2(key, kdfParameters);
break; break;
case KdfType.Aes: case KdfType.Aes:
_logger.fine('Must be using aes'); _logger.fine('Must be using aes');
@ -97,8 +97,9 @@ class KeyEncrypterKdf {
'unsupported KDF Type UUID ${ByteUtils.toHexList(uuid)}.'); 'unsupported KDF Type UUID ${ByteUtils.toHexList(uuid)}.');
} }
Uint8List encryptArgon2(Uint8List key, VarDictionary kdfParameters) { Future<Uint8List> encryptArgon2(
return argon2.argon2( Uint8List key, VarDictionary kdfParameters) async {
return await argon2.argon2Async(Argon2Arguments(
key, key,
KdfField.salt.read(kdfParameters), KdfField.salt.read(kdfParameters),
// 65536, //KdfField.memory.read(kdfParameters), // 65536, //KdfField.memory.read(kdfParameters),
@ -108,7 +109,7 @@ class KeyEncrypterKdf {
KdfField.parallelism.read(kdfParameters), KdfField.parallelism.read(kdfParameters),
0, 0,
KdfField.version.read(kdfParameters), KdfField.version.read(kdfParameters),
); ));
} }
Uint8List encryptAes(Uint8List key, VarDictionary kdfParameters) { Uint8List encryptAes(Uint8List key, VarDictionary kdfParameters) {

26
lib/src/kdbx_format.dart

@ -149,7 +149,7 @@ class KdbxFile {
Stream<Set<KdbxObject>> get dirtyObjectsChanged => Stream<Set<KdbxObject>> get dirtyObjectsChanged =>
_dirtyObjectsChanged.stream; _dirtyObjectsChanged.stream;
Uint8List save() { Future<Uint8List> save() async {
final output = BytesBuilder(); final output = BytesBuilder();
final writer = WriterHelper(output); final writer = WriterHelper(output);
header.generateSalts(); header.generateSalts();
@ -167,7 +167,7 @@ class KdbxFile {
final headerBytes = writer.output.toBytes(); final headerBytes = writer.output.toBytes();
writer.writeBytes(headerHash); writer.writeBytes(headerHash);
final gen = kdbxFormat._createProtectedSaltGenerator(header); final gen = kdbxFormat._createProtectedSaltGenerator(header);
final keys = kdbxFormat._computeKeysV4(header, credentials); final keys = await kdbxFormat._computeKeysV4(header, credentials);
final headerHmac = kdbxFormat._getHeaderHmac(headerBytes, keys.hmacKey); final headerHmac = kdbxFormat._getHeaderHmac(headerBytes, keys.hmacKey);
writer.writeBytes(headerHmac.bytes as Uint8List); writer.writeBytes(headerHmac.bytes as Uint8List);
body.writeV4(writer, this, gen, keys); body.writeV4(writer, this, gen, keys);
@ -365,13 +365,13 @@ class KdbxFormat {
); );
} }
KdbxFile read(Uint8List input, Credentials credentials) { Future<KdbxFile> read(Uint8List input, Credentials credentials) async {
final reader = ReaderHelper(input); final reader = ReaderHelper(input);
final header = KdbxHeader.read(reader); final header = KdbxHeader.read(reader);
if (header.versionMajor == 3) { if (header.versionMajor == 3) {
return _loadV3(header, reader, credentials); return _loadV3(header, reader, credentials);
} else if (header.versionMajor == 4) { } else if (header.versionMajor == 4) {
return _loadV4(header, reader, credentials); return await _loadV4(header, reader, credentials);
} else { } else {
_logger.finer('Unsupported version for $header'); _logger.finer('Unsupported version for $header');
throw KdbxUnsupportedException('Unsupported kdbx version ' throw KdbxUnsupportedException('Unsupported kdbx version '
@ -399,20 +399,20 @@ class KdbxFormat {
} }
} }
KdbxFile _loadV4( Future<KdbxFile> _loadV4(
KdbxHeader header, ReaderHelper reader, Credentials credentials) { KdbxHeader header, ReaderHelper reader, Credentials credentials) async {
final headerBytes = reader.byteData.sublist(0, header.endPos); final headerBytes = reader.byteData.sublist(0, header.endPos);
final hash = crypto.sha256.convert(headerBytes).bytes; final hash = crypto.sha256.convert(headerBytes).bytes;
final actualHash = reader.readBytes(hash.length); final actualHash = reader.readBytes(hash.length);
if (!ByteUtils.eq(hash, actualHash)) { if (!ByteUtils.eq(hash, actualHash)) {
_logger.fine( _logger.fine('Does not match ${ByteUtils.toHexList(hash)} '
'Does not match ${ByteUtils.toHexList(hash)} vs ${ByteUtils.toHexList(actualHash)}'); 'vs ${ByteUtils.toHexList(actualHash)}');
throw KdbxCorruptedFileException('Header hash does not match.'); throw KdbxCorruptedFileException('Header hash does not match.');
} }
// _logger // _logger
// .finest('KdfParameters: ${header.readKdfParameters.toDebugString()}'); // .finest('KdfParameters: ${header.readKdfParameters.toDebugString()}');
_logger.finest('Header hash matches.'); _logger.finest('Header hash matches.');
final keys = _computeKeysV4(header, credentials); final keys = await _computeKeysV4(header, credentials);
final headerHmac = final headerHmac =
_getHeaderHmac(reader.byteData.sublist(0, header.endPos), keys.hmacKey); _getHeaderHmac(reader.byteData.sublist(0, header.endPos), keys.hmacKey);
final expectedHmac = reader.readBytes(headerHmac.bytes.length); final expectedHmac = reader.readBytes(headerHmac.bytes.length);
@ -435,7 +435,7 @@ class KdbxFormat {
final xml = utf8.decode(contentReader.readRemaining()); final xml = utf8.decode(contentReader.readRemaining());
return KdbxFile(this, credentials, header, _loadXml(header, xml)); return KdbxFile(this, credentials, header, _loadXml(header, xml));
} }
return null; throw StateError('Kdbx4 without compression is not yet supported.');
} }
Uint8List hmacBlockTransformerEncrypt(Uint8List hmacKey, Uint8List data) { Uint8List hmacBlockTransformerEncrypt(Uint8List hmacKey, Uint8List data) {
@ -539,7 +539,8 @@ class KdbxFormat {
return hmacKeyStuff.convert(src); return hmacKeyStuff.convert(src);
} }
_KeysV4 _computeKeysV4(KdbxHeader header, Credentials credentials) { Future<_KeysV4> _computeKeysV4(
KdbxHeader header, Credentials credentials) async {
final masterSeed = header.fields[HeaderFields.MasterSeed].bytes; final masterSeed = header.fields[HeaderFields.MasterSeed].bytes;
final kdfParameters = header.readKdfParameters; final kdfParameters = header.readKdfParameters;
if (masterSeed.length != 32) { if (masterSeed.length != 32) {
@ -547,7 +548,8 @@ class KdbxFormat {
} }
final credentialHash = credentials.getHash(); final credentialHash = credentials.getHash();
final key = KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters); final key =
await KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters);
// final keyWithSeed = Uint8List(65); // final keyWithSeed = Uint8List(65);
// keyWithSeed.replaceRange(0, masterSeed.length, masterSeed); // keyWithSeed.replaceRange(0, masterSeed.length, masterSeed);

41
test/kdbx4_test.dart

@ -1,7 +1,5 @@
import 'dart:convert';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
@ -60,21 +58,17 @@ void main() {
final kdbxFormat = KdbxFormat(Argon2Test()); final kdbxFormat = KdbxFormat(Argon2Test());
group('Reading', () { group('Reading', () {
test('bubb', () async { test('bubb', () async {
final key = utf8.encode('asdf') as Uint8List;
final salt = Uint8List(8);
// final result = Argon2Test().argon2(key, salt, 1 << 16, 5, 16, 1, 0x13, 1);
// _logger.fine('hashing: $result');
final data = await File('test/keepassxcpasswords.kdbx').readAsBytes(); final data = await File('test/keepassxcpasswords.kdbx').readAsBytes();
final file = final file = await kdbxFormat.read(
kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf'))); data, Credentials(ProtectedValue.fromString('asdf')));
final firstEntry = file.body.rootGroup.entries.first; final firstEntry = file.body.rootGroup.entries.first;
final pwd = firstEntry.getString(KdbxKey('Password')).getText(); final pwd = firstEntry.getString(KdbxKey('Password')).getText();
expect(pwd, 'MyPassword'); expect(pwd, 'MyPassword');
}); });
test('Reading kdbx4_keeweb', () async { test('Reading kdbx4_keeweb', () async {
final data = await File('test/kdbx4_keeweb.kdbx').readAsBytes(); final data = await File('test/kdbx4_keeweb.kdbx').readAsBytes();
final file = final file = await kdbxFormat.read(
kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf'))); data, Credentials(ProtectedValue.fromString('asdf')));
final firstEntry = file.body.rootGroup.entries.first; final firstEntry = file.body.rootGroup.entries.first;
final pwd = firstEntry.getString(KdbxKey('Password')).getText(); final pwd = firstEntry.getString(KdbxKey('Password')).getText();
expect(pwd, 'def'); expect(pwd, 'def');
@ -84,25 +78,25 @@ void main() {
await File('test/keyfile/BinaryKeyFilePasswords.kdbx').readAsBytes(); await File('test/keyfile/BinaryKeyFilePasswords.kdbx').readAsBytes();
final keyFile = final keyFile =
await File('test/keyfile/binarykeyfile.key').readAsBytes(); await File('test/keyfile/binarykeyfile.key').readAsBytes();
final file = kdbxFormat.read(data, final file = await kdbxFormat.read(data,
Credentials.composite(ProtectedValue.fromString('asdf'), keyFile)); Credentials.composite(ProtectedValue.fromString('asdf'), keyFile));
expect(file.body.rootGroup.entries, hasLength(1)); expect(file.body.rootGroup.entries, hasLength(1));
}); });
test('Reading chacha20', () async { test('Reading chacha20', () async {
final data = await File('test/chacha20.kdbx').readAsBytes(); final data = await File('test/chacha20.kdbx').readAsBytes();
final file = final file = await kdbxFormat.read(
kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf'))); data, Credentials(ProtectedValue.fromString('asdf')));
expect(file.body.rootGroup.entries, hasLength(1)); expect(file.body.rootGroup.entries, hasLength(1));
}); });
test('Reading aes-kdf', () async { test('Reading aes-kdf', () async {
final data = await File('test/aeskdf.kdbx').readAsBytes(); final data = await File('test/aeskdf.kdbx').readAsBytes();
final file = final file = await kdbxFormat.read(
kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf'))); data, Credentials(ProtectedValue.fromString('asdf')));
expect(file.body.rootGroup.entries, hasLength(1)); expect(file.body.rootGroup.entries, hasLength(1));
}); });
}); });
group('Writing', () { group('Writing', () {
test('Create and save', () { test('Create and save', () async {
final credentials = Credentials(ProtectedValue.fromString('asdf')); final credentials = Credentials(ProtectedValue.fromString('asdf'));
final kdbx = kdbxFormat.create( final kdbx = kdbxFormat.create(
credentials, credentials,
@ -112,27 +106,28 @@ void main() {
final rootGroup = kdbx.body.rootGroup; final rootGroup = kdbx.body.rootGroup;
_createEntry(kdbx, rootGroup, 'user1', 'LoremIpsum'); _createEntry(kdbx, rootGroup, 'user1', 'LoremIpsum');
_createEntry(kdbx, rootGroup, 'user2', 'Second Password'); _createEntry(kdbx, rootGroup, 'user2', 'Second Password');
final saved = kdbx.save(); final saved = await kdbx.save();
final loadedKdbx = kdbxFormat.read( final loadedKdbx = await kdbxFormat.read(
saved, Credentials(ProtectedValue.fromString('asdf'))); saved, Credentials(ProtectedValue.fromString('asdf')));
_logger.fine('Successfully loaded kdbx $loadedKdbx'); _logger.fine('Successfully loaded kdbx $loadedKdbx');
File('test_v4x.kdbx').writeAsBytesSync(saved); File('test_v4x.kdbx').writeAsBytesSync(saved);
}); });
test('Reading it', () async { test('Reading it', () async {
final data = await File('test/test_v4x.kdbx').readAsBytes(); final data = await File('test/test_v4x.kdbx').readAsBytes();
final file = final file = await kdbxFormat.read(
kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf'))); data, Credentials(ProtectedValue.fromString('asdf')));
_logger.fine('successfully read ${file.body.rootGroup.name}');
}); });
test('write chacha20', () async { test('write chacha20', () async {
final data = await File('test/chacha20.kdbx').readAsBytes(); final data = await File('test/chacha20.kdbx').readAsBytes();
final file = final file = await kdbxFormat.read(
kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf'))); data, Credentials(ProtectedValue.fromString('asdf')));
expect(file.body.rootGroup.entries, hasLength(1)); expect(file.body.rootGroup.entries, hasLength(1));
_createEntry(file, file.body.rootGroup, 'user1', 'LoremIpsum'); _createEntry(file, file.body.rootGroup, 'user1', 'LoremIpsum');
// and try to write it. // and try to write it.
final output = file.save(); final output = await file.save();
expect(output, isNotNull); expect(output, isNotNull);
File('test_output_chacha20.kdbx').writeAsBytesSync(output); File('test_output_chacha20.kdbx').writeAsBytesSync(output);
}); });

14
test/kdbx_test.dart

@ -1,4 +1,3 @@
import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
@ -27,7 +26,8 @@ void main() {
test('First Test', () async { test('First Test', () async {
final data = await File('test/FooBar.kdbx').readAsBytes(); final data = await File('test/FooBar.kdbx').readAsBytes();
kdbxForamt.read(data, Credentials(ProtectedValue.fromString('FooBar'))); await kdbxForamt.read(
data, Credentials(ProtectedValue.fromString('FooBar')));
}); });
}); });
@ -38,7 +38,7 @@ void main() {
final cred = Credentials.composite( final cred = Credentials.composite(
ProtectedValue.fromString('asdf'), keyFileBytes); ProtectedValue.fromString('asdf'), keyFileBytes);
final data = await File('test/password-and-keyfile.kdbx').readAsBytes(); final data = await File('test/password-and-keyfile.kdbx').readAsBytes();
final file = kdbxForamt.read(data, cred); final file = await kdbxForamt.read(data, cred);
expect(file.body.rootGroup.entries, hasLength(2)); expect(file.body.rootGroup.entries, hasLength(2));
}); });
test('Read with PW and hex keyfile', () async { test('Read with PW and hex keyfile', () async {
@ -47,7 +47,7 @@ void main() {
final cred = Credentials.composite( final cred = Credentials.composite(
ProtectedValue.fromString('testing99'), keyFileBytes); ProtectedValue.fromString('testing99'), keyFileBytes);
final data = await File('test/keyfile/newdatabase2.kdbx').readAsBytes(); final data = await File('test/keyfile/newdatabase2.kdbx').readAsBytes();
final file = kdbxForamt.read(data, cred); final file = await kdbxForamt.read(data, cred);
expect(file.body.rootGroup.entries, hasLength(3)); expect(file.body.rootGroup.entries, hasLength(3));
}); });
}); });
@ -79,9 +79,9 @@ void main() {
}); });
group('Integration', () { group('Integration', () {
test('Simple save and load', () { test('Simple save and load', () async {
final credentials = Credentials(ProtectedValue.fromString('FooBar')); final credentials = Credentials(ProtectedValue.fromString('FooBar'));
final Uint8List saved = (() { final Uint8List saved = await (() async {
final kdbx = kdbxForamt.create(credentials, 'CreateTest'); final kdbx = kdbxForamt.create(credentials, 'CreateTest');
final rootGroup = kdbx.body.rootGroup; final rootGroup = kdbx.body.rootGroup;
final entry = KdbxEntry.create(kdbx, rootGroup); final entry = KdbxEntry.create(kdbx, rootGroup);
@ -93,7 +93,7 @@ void main() {
// print(ByteUtils.toHexList(saved)); // print(ByteUtils.toHexList(saved));
final kdbx = kdbxForamt.read(saved, credentials); final kdbx = await kdbxForamt.read(saved, credentials);
expect( expect(
kdbx.body.rootGroup.entries.first kdbx.body.rootGroup.entries.first
.getString(KdbxKey('Password')) .getString(KdbxKey('Password'))

Loading…
Cancel
Save