Browse Source

a bit of refactoring

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
fdc6f149dc
  1. 1
      .idea/dictionaries/herbert.xml
  2. 11
      README.md
  3. 19
      lib/src/internal/byte_utils.dart
  4. 84
      lib/src/internal/crypto_utils.dart
  5. 83
      lib/src/kdbx_format.dart
  6. 162
      lib/src/kdbx_header.dart
  7. 1
      pubspec.yaml
  8. 80
      test/FooBar.content.xml
  9. BIN
      test/FooBar.kdbx
  10. 1
      test/kdbx_test.dart

1
.idea/dictionaries/herbert.xml

@ -2,6 +2,7 @@
<dictionary name="herbert"> <dictionary name="herbert">
<words> <words>
<w>consts</w> <w>consts</w>
<w>derivator</w>
<w>kdbx</w> <w>kdbx</w>
</words> </words>
</dictionary> </dictionary>

11
README.md

@ -1,5 +1,7 @@
# kdbx.dart # kdbx.dart
**this is just an experiment right now**
KeepassX format implementation in pure dart. KeepassX format implementation in pure dart.
Very much based on https://github.com/keeweb/kdbxweb/ Very much based on https://github.com/keeweb/kdbxweb/
@ -11,3 +13,12 @@ TODO
## Features and bugs ## Features and bugs
* Only supports v3. * Only supports v3.
# TODO
* For v4 argon2 support would be required. Unfortunately there are no dart
implementations, or bindings yet. (as far as I can find).
* Reference implementation: https://github.com/P-H-C/phc-winner-argon2
* Rust: https://github.com/bryant/argon2rs/blob/master/src/argon2.rs
* C#: https://github.com/mheyman/Isopoh.Cryptography.Argon2

19
lib/src/internal/byte_utils.dart

@ -0,0 +1,19 @@
import 'dart:typed_data';
class ByteUtils {
static bool eq(Uint8List a, Uint8List b) {
if (a.length != b.length) {
return false;
}
for (int i = a.length - 1; i >= 0; i--) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
static String toHex(int val) => '0x${val.toRadixString(16)}';
static String toHexList(Uint8List list) => list.map((val) => toHex(val)).join(' ');
}

84
lib/src/internal/crypto_utils.dart

@ -0,0 +1,84 @@
import 'dart:typed_data';
import 'package:pointycastle/export.dart';
class CryptoUtils {
}
/// https://gist.github.com/proteye/e54eef1713e1fe9123d1eb04c0a5cf9b
class AesHelper {
static const CBC_MODE = 'CBC';
static const CFB_MODE = 'CFB';
// AES key size
static const KEY_SIZE = 32; // 32 byte key for AES-256
static const ITERATION_COUNT = 1000;
static Uint8List deriveKey(
Uint8List password, {
Uint8List salt,
int iterationCount = ITERATION_COUNT,
int derivedKeyLength = KEY_SIZE,
}) {
final Pbkdf2Parameters params = Pbkdf2Parameters(salt, iterationCount, derivedKeyLength);
final KeyDerivator keyDerivator = PBKDF2KeyDerivator(HMac(SHA256Digest(), 16));
keyDerivator.init(params);
return keyDerivator.process(password);
}
static String decrypt(Uint8List derivedKey, Uint8List cipherIvBytes, {String mode = CBC_MODE}) {
// Uint8List derivedKey = deriveKey(password);
final KeyParameter keyParam = KeyParameter(derivedKey);
final BlockCipher aes = AESFastEngine();
// Uint8List cipherIvBytes = base64.decode(ciphertext);
final Uint8List iv = Uint8List(aes.blockSize)..setRange(0, aes.blockSize, cipherIvBytes);
BlockCipher cipher;
final ParametersWithIV params = ParametersWithIV(keyParam, iv);
switch (mode) {
case CBC_MODE:
cipher = CBCBlockCipher(aes);
break;
case CFB_MODE:
cipher = CFBBlockCipher(aes, aes.blockSize);
break;
default:
throw ArgumentError('incorrect value of the "mode" parameter');
break;
}
cipher.init(false, params);
final int cipherLen = cipherIvBytes.length - aes.blockSize;
final Uint8List cipherBytes = Uint8List(cipherLen)..setRange(0, cipherLen, cipherIvBytes, aes.blockSize);
final Uint8List paddedText = processBlocks(cipher, cipherBytes);
final Uint8List textBytes = unpad(paddedText);
return String.fromCharCodes(textBytes);
}
static Uint8List unpad(Uint8List src) {
final pad = PKCS7Padding();
pad.init(null);
final int padLength = pad.padCount(src);
final int len = src.length - padLength;
return Uint8List(len)..setRange(0, len, src);
}
static Uint8List processBlocks(BlockCipher cipher, Uint8List inp) {
final out = Uint8List(inp.lengthInBytes);
for (var offset = 0; offset < inp.lengthInBytes;) {
final len = cipher.processBlock(inp, offset, out, offset);
offset += len;
}
return out;
}
}

83
lib/src/kdbx_format.dart

@ -0,0 +1,83 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/src/internal/byte_utils.dart';
import 'package:kdbx/src/internal/crypto_utils.dart';
import 'package:kdbx/src/kdbx_header.dart';
import 'package:logging/logging.dart';
import 'package:pointycastle/export.dart';
final _logger = Logger('kdbx.format');
class KdbxFormat {
static Future<void> read(Uint8List input, Credentials credentials) async {
final reader = ReaderHelper(input);
final header = await KdbxHeader.read(reader);
_loadV3(header, reader, credentials);
}
static void _loadV3(KdbxHeader header, ReaderHelper reader, Credentials credentials) {
// _getMasterKeyV3(header, credentials);
final masterKey = _generateMasterKeyV3(header, credentials);
final encryptedPayload = reader.readRemaining();
final content = _decryptContent(header, masterKey, encryptedPayload);
final blocks = HashedBlockReader.readBlocks(ReaderHelper(content));
_logger.finer('compression: ${header.compression}');
if (header.compression == Compression.gzip) {
final xml = GZipCodec().decode(blocks);
final string = utf8.decode(xml);
print('xml: $string');
}
// final result = utf8.decode(decrypted);
// final aesEngine = AESFastEngine();
// aesEngine.init(true, KeyParameter(seed));
// final key = AesHelper.deriveKey(keyComposite.bytes as Uint8List, salt: seed, iterationCount: rounds, derivedKeyLength: 32);
// final masterKey = Uint8List.fromList(key + masterSeed.asUint8List());
// print('key length: ${key.length} + ${masterSeed.lengthInBytes} = ${masterKey.lengthInBytes} (${masterKey.lengthInBytes} bytes)');
// final result = AesHelper.decrypt(masterKey, reader.readRemaining());
// print('before : ${_toHexList(encryptedPayload)}');
}
static Uint8List _decryptContent(KdbxHeader header, Uint8List masterKey, Uint8List encryptedPayload) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
final decryptCipher = CBCBlockCipher(AESFastEngine());
decryptCipher.init(false, ParametersWithIV(KeyParameter(masterKey), encryptionIv.asUint8List()));
final decrypted = AesHelper.processBlocks(decryptCipher, encryptedPayload);
final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes;
_logger.finest('streamStart: ${ByteUtils.toHexList(streamStart.asUint8List())}');
_logger.finest('actual : ${ByteUtils.toHexList(decrypted.sublist(0, streamStart.lengthInBytes))}');
if (!ByteUtils.eq(streamStart.asUint8List(), decrypted.sublist(0, streamStart.lengthInBytes))) {
throw KdbxInvalidKeyException();
}
final content = decrypted.sublist(streamStart.lengthInBytes);
return content;
}
static Uint8List _generateMasterKeyV3(KdbxHeader header, Credentials credentials) {
final rounds = header.fields[HeaderFields.TransformRounds].bytes.asUint64List().first;
final seed = header.fields[HeaderFields.TransformSeed].bytes.asUint8List();
final masterSeed = header.fields[HeaderFields.MasterSeed].bytes;
_logger.finer('Rounds: $rounds');
final cipher = ECBBlockCipher(AESFastEngine())..init(true, KeyParameter(seed));
final pwHash = credentials.getHash();
var transformedKey = pwHash;
for (int i = 0; i < rounds; i++) {
transformedKey = AesHelper.processBlocks(cipher, transformedKey);
}
transformedKey = crypto.sha256.convert(transformedKey).bytes as Uint8List;
final masterKey =
crypto.sha256.convert(Uint8List.fromList(masterSeed.asUint8List() + transformedKey)).bytes as Uint8List;
return masterKey;
}
}

162
lib/src/kdbx_header.dart

@ -5,6 +5,7 @@ import 'dart:typed_data';
import 'package:convert/convert.dart' as convert; import 'package:convert/convert.dart' as convert;
import 'package:crypto/crypto.dart' as crypto; import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/src/crypto/protected_value.dart'; import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/internal/byte_utils.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
@ -49,10 +50,6 @@ class HeaderField {
String get name => field.toString(); String get name => field.toString();
} }
String _toHex(int val) => '0x${val.toRadixString(16)}';
String _toHexList(Uint8List list) => list.map((val) => _toHex(val)).join(' ');
class KdbxHeader { class KdbxHeader {
KdbxHeader({this.sig1, this.sig2, this.versionMinor, this.versionMajor, this.fields}); KdbxHeader({this.sig1, this.sig2, this.versionMinor, this.versionMajor, this.fields});
@ -61,7 +58,7 @@ class KdbxHeader {
final sig1 = reader.readUint32(); final sig1 = reader.readUint32();
final sig2 = reader.readUint32(); final sig2 = reader.readUint32();
if (!(sig1 == Consts.FileMagic && sig2 == Consts.Sig2Kdbx)) { if (!(sig1 == Consts.FileMagic && sig2 == Consts.Sig2Kdbx)) {
throw UnsupportedError('Unsupported file structure. ${_toHex(sig1)}, ${_toHex(sig2)}'); throw UnsupportedError('Unsupported file structure. ${ByteUtils.toHex(sig1)}, ${ByteUtils.toHex(sig2)}');
} }
// reading version // reading version
@ -137,86 +134,7 @@ class KdbxUnsupportedException implements KdbxException {
final String hint; final String hint;
} }
class KdbxFormat {
static Future<void> read(Uint8List input, Credentials credentials) async {
final reader = ReaderHelper(input);
final header = await KdbxHeader.read(reader);
_loadV3(header, reader, credentials);
}
static void _loadV3(KdbxHeader header, ReaderHelper reader, Credentials credentials) {
// _getMasterKeyV3(header, credentials);
final pwHash = credentials.getHash();
final seed = header.fields[HeaderFields.TransformSeed].bytes.asUint8List();
final rounds = header.fields[HeaderFields.TransformRounds].bytes.asUint64List().first;
final masterSeed = header.fields[HeaderFields.MasterSeed].bytes;
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
_logger.finer('Rounds: $rounds');
final cipher = ECBBlockCipher(AESFastEngine());
final encryptedPayload = reader.readRemaining();
cipher.init(true, KeyParameter(seed));
var transformedKey = pwHash;
for (int i = 0; i < rounds; i++) {
transformedKey = AesHelper._processBlocks(cipher, transformedKey);
}
transformedKey = crypto.sha256.convert(transformedKey).bytes as Uint8List;
final masterKey =
crypto.sha256.convert(Uint8List.fromList(masterSeed.asUint8List() + transformedKey)).bytes as Uint8List;
final decryptCipher = CBCBlockCipher(AESFastEngine());
decryptCipher.init(false, ParametersWithIV(KeyParameter(masterKey), encryptionIv.asUint8List()));
// final decrypted = decryptCipher.process(encryptedPayload);
final decrypted = AesHelper._processBlocks(decryptCipher, encryptedPayload);
final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes;
print('streamStart: ${_toHexList(streamStart.asUint8List())}');
print('actual : ${_toHexList(decrypted.sublist(0, streamStart.lengthInBytes))}');
if (!_eq(streamStart.asUint8List(), decrypted.sublist(0, streamStart.lengthInBytes))) {
throw KdbxInvalidKeyException();
}
final content = decrypted.sublist(streamStart.lengthInBytes);
final blocks = HashedBlockReader.readBlocks(ReaderHelper(content));
print('compression: ${header.compression}');
if (header.compression == Compression.gzip) {
final xml = GZipCodec().decode(blocks);
final string = utf8.decode(xml);
print('xml: $string');
}
// final result = utf8.decode(decrypted);
// final aesEngine = AESFastEngine();
// aesEngine.init(true, KeyParameter(seed));
// final key = AesHelper.deriveKey(keyComposite.bytes as Uint8List, salt: seed, iterationCount: rounds, derivedKeyLength: 32);
// final masterKey = Uint8List.fromList(key + masterSeed.asUint8List());
// print('key length: ${key.length} + ${masterSeed.lengthInBytes} = ${masterKey.lengthInBytes} (${masterKey.lengthInBytes} bytes)');
// final result = AesHelper.decrypt(masterKey, reader.readRemaining());
print('before : ${_toHexList(encryptedPayload)}');
}
static void _getMasterKeyV3(KdbxHeader header, Credentials credentials) {
final pwHash = credentials.getHash();
final seed = header.fields[HeaderFields.TransformSeed].bytes.asUint8List();
final rounds = header.fields[HeaderFields.TransformRounds].bytes.asUint64List().first;
final masterSeed = header.fields[HeaderFields.MasterSeed].bytes;
final key = AesHelper.deriveKey(pwHash, salt: seed, iterationCount: rounds);
}
}
bool _eq(Uint8List a, Uint8List b) {
if (a.length != b.length) {
return false;
}
for (int i = a.length - 1; i >= 0; i--) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
class HashedBlockReader { class HashedBlockReader {
static Uint8List readBlocks(ReaderHelper reader) => static Uint8List readBlocks(ReaderHelper reader) =>
@ -229,7 +147,7 @@ class HashedBlockReader {
final blockSize = reader.readUint32(); final blockSize = reader.readUint32();
if (blockSize > 0) { if (blockSize > 0) {
final blockData = reader.readBytes(blockSize).asUint8List(); final blockData = reader.readBytes(blockSize).asUint8List();
if (!_eq(crypto.sha256.convert(blockData).bytes as Uint8List, blockHash.asUint8List())) { if (!ByteUtils.eq(crypto.sha256.convert(blockData).bytes as Uint8List, blockHash.asUint8List())) {
throw KdbxCorruptedFileException(); throw KdbxCorruptedFileException();
} }
yield blockData; yield blockData;
@ -259,77 +177,3 @@ class ReaderHelper {
Uint8List readRemaining() => data.sublist(pos); Uint8List readRemaining() => data.sublist(pos);
} }
/// https://gist.github.com/proteye/e54eef1713e1fe9123d1eb04c0a5cf9b
class AesHelper {
static const CBC_MODE = 'CBC';
static const CFB_MODE = 'CFB';
// AES key size
static const KEY_SIZE = 32; // 32 byte key for AES-256
static const ITERATION_COUNT = 1000;
static Uint8List deriveKey(
Uint8List password, {
Uint8List salt,
int iterationCount = ITERATION_COUNT,
int derivedKeyLength = KEY_SIZE,
}) {
Pbkdf2Parameters params = Pbkdf2Parameters(salt, iterationCount, derivedKeyLength);
KeyDerivator keyDerivator = PBKDF2KeyDerivator(HMac(SHA256Digest(), 16));
keyDerivator.init(params);
return keyDerivator.process(password);
}
static String decrypt(Uint8List derivedKey, Uint8List cipherIvBytes, {String mode = CBC_MODE}) {
// Uint8List derivedKey = deriveKey(password);
KeyParameter keyParam = KeyParameter(derivedKey);
BlockCipher aes = AESFastEngine();
// Uint8List cipherIvBytes = base64.decode(ciphertext);
Uint8List iv = Uint8List(aes.blockSize)..setRange(0, aes.blockSize, cipherIvBytes);
BlockCipher cipher;
ParametersWithIV params = ParametersWithIV(keyParam, iv);
switch (mode) {
case CBC_MODE:
cipher = CBCBlockCipher(aes);
break;
case CFB_MODE:
cipher = CFBBlockCipher(aes, aes.blockSize);
break;
default:
throw ArgumentError('incorrect value of the "mode" parameter');
break;
}
cipher.init(false, params);
int cipherLen = cipherIvBytes.length - aes.blockSize;
Uint8List cipherBytes = new Uint8List(cipherLen)..setRange(0, cipherLen, cipherIvBytes, aes.blockSize);
Uint8List paddedText = _processBlocks(cipher, cipherBytes);
Uint8List textBytes = unpad(paddedText);
return String.fromCharCodes(textBytes);
}
static Uint8List unpad(Uint8List src) {
final pad = PKCS7Padding();
pad.init(null);
int padLength = pad.padCount(src);
int len = src.length - padLength;
return Uint8List(len)..setRange(0, len, src);
}
static Uint8List _processBlocks(BlockCipher cipher, Uint8List inp) {
var out = Uint8List(inp.lengthInBytes);
for (var offset = 0; offset < inp.lengthInBytes;) {
var len = cipher.processBlock(inp, offset, out, offset);
offset += len;
}
return out;
}
}

1
pubspec.yaml

@ -12,6 +12,7 @@ dependencies:
logging: '>=0.11.3+2 <1.0.0' logging: '>=0.11.3+2 <1.0.0'
crypto: '>=2.0.0 <3.0.0' crypto: '>=2.0.0 <3.0.0'
pointycastle: ^1.0.1 pointycastle: ^1.0.1
xml: ^3.5.0
dev_dependencies: dev_dependencies:
logging_appenders: '>=0.1.0 <1.0.0' logging_appenders: '>=0.1.0 <1.0.0'

80
test/FooBar.content.xml

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<KeePassFile>
<Meta>
<Generator>KdbxWeb</Generator>
<HeaderHash>a9XeOPjkxVOzggrVtvoEpBIc07uqIShumzKMU+/lj04=</HeaderHash>
<DatabaseName>FooBar</DatabaseName>
<DatabaseNameChanged>2019-08-20T13:16:06Z</DatabaseNameChanged>
<DatabaseDescription />
<DatabaseDescriptionChanged>2019-08-20T13:15:47Z</DatabaseDescriptionChanged>
<DefaultUserName />
<DefaultUserNameChanged>2019-08-20T13:15:47Z</DefaultUserNameChanged>
<MaintenanceHistoryDays>365</MaintenanceHistoryDays>
<Color />
<MasterKeyChanged>2019-08-20T13:16:03Z</MasterKeyChanged>
<MasterKeyChangeRec>-1</MasterKeyChangeRec>
<MasterKeyChangeForce>-1</MasterKeyChangeForce>
<RecycleBinEnabled>True</RecycleBinEnabled>
<RecycleBinUUID>dVSBC/BAx70qcsy6XkrGJA==</RecycleBinUUID>
<RecycleBinChanged>2019-08-20T13:15:47Z</RecycleBinChanged>
<EntryTemplatesGroup>AAAAAAAAAAAAAAAAAAAAAA==</EntryTemplatesGroup>
<EntryTemplatesGroupChanged>2019-08-20T13:15:47Z</EntryTemplatesGroupChanged>
<HistoryMaxItems>10</HistoryMaxItems>
<HistoryMaxSize>6291456</HistoryMaxSize>
<LastSelectedGroup />
<LastTopVisibleGroup />
<MemoryProtection>
<ProtectTitle>False</ProtectTitle>
<ProtectUserName>False</ProtectUserName>
<ProtectPassword>True</ProtectPassword>
<ProtectURL>False</ProtectURL>
<ProtectNotes>False</ProtectNotes>
</MemoryProtection>
<CustomIcons />
<Binaries />
<CustomData />
</Meta>
<Root>
<Group>
<UUID>LAQMkihXTkxhA2D2tE40Fg==</UUID>
<Name>FooBar</Name>
<Notes />
<IconID>49</IconID>
<Times>
<CreationTime>2019-08-20T13:15:47Z</CreationTime>
<LastModificationTime>2019-08-20T13:16:06Z</LastModificationTime>
<LastAccessTime>2019-08-20T13:16:06Z</LastAccessTime>
<ExpiryTime>2019-08-20T13:15:47Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>2019-08-20T13:15:47Z</LocationChanged>
</Times>
<IsExpanded>True</IsExpanded>
<DefaultAutoTypeSequence />
<EnableAutoType>null</EnableAutoType>
<EnableSearching>null</EnableSearching>
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
<Group>
<UUID>dVSBC/BAx70qcsy6XkrGJA==</UUID>
<Name>Recycle Bin</Name>
<Notes />
<IconID>43</IconID>
<Times>
<CreationTime>2019-08-20T13:15:47Z</CreationTime>
<LastModificationTime>2019-08-20T13:15:47Z</LastModificationTime>
<LastAccessTime>2019-08-20T13:15:47Z</LastAccessTime>
<ExpiryTime>2019-08-20T13:15:47Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>2019-08-20T13:15:47Z</LocationChanged>
</Times>
<IsExpanded>True</IsExpanded>
<DefaultAutoTypeSequence />
<EnableAutoType>False</EnableAutoType>
<EnableSearching>False</EnableSearching>
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
</Group>
</Group>
<DeletedObjects />
</Root>
</KeePassFile>

BIN
test/FooBar.kdbx

Binary file not shown.

1
test/kdbx_test.dart

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/crypto/protected_value.dart'; import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_header.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart'; import 'package:logging_appenders/logging_appenders.dart';

Loading…
Cancel
Save