Browse Source

basic support for decrypting kdbx 3.x files

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
4280997bf5
  1. 5
      .idea/codeStyles/codeStyleConfig.xml
  2. 5
      README.md
  3. 91
      bin/kdbx.dart
  4. 25
      lib/src/crypto/protected_salt_generator.dart
  5. 4
      lib/src/internal/crypto_utils.dart
  6. 5
      lib/src/kdbx_entry.dart
  7. 97
      lib/src/kdbx_format.dart
  8. 4
      lib/src/kdbx_group.dart
  9. 16
      lib/src/kdbx_header.dart
  10. 10
      pubspec.yaml
  11. 122
      test/FooBar.2.content.xml

5
.idea/codeStyles/codeStyleConfig.xml

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

5
README.md

@ -4,7 +4,10 @@
KeepassX format implementation in pure dart.
Very much based on https://github.com/keeweb/kdbxweb/
## Resources
* Code is very much based on https://github.com/keeweb/kdbxweb/
* https://gist.github.com/msmuenchen/9318327
## Usage

91
bin/kdbx.dart

@ -0,0 +1,91 @@
import 'dart:async';
import 'dart:io';
import 'package:args/args.dart';
import 'package:args/command_runner.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:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';
import 'package:prompts/prompts.dart' as prompts;
final _logger = Logger('kdbx');
void main(List<String> arguments) {
exitCode = 0;
final runner = KdbxCommandRunner('kdbx', 'Kdbx Utility');
runner.run(arguments).catchError((dynamic error, StackTrace stackTrace) {
if (error is! UsageException) {
return Future<dynamic>.error(error, stackTrace);
}
print(error);
exit(64);
return null;
});
// final inputFile = args['input'] as String;
// if (inputFile == null) {
// print('Missing Argument --input');
// print(parser.usage);
// exitCode = 1;
// return;
// }
_logger.info('done.');
}
class KdbxCommandRunner extends CommandRunner<void> {
KdbxCommandRunner(String executableName, String description) : super(executableName, description) {
argParser.addFlag('verbose', abbr: 'v');
addCommand(CatCommand());
}
@override
Future<void> runCommand(ArgResults topLevelResults) {
PrintAppender().attachToLogger(Logger.root);
Logger.root.level = Level.INFO;
if (topLevelResults['verbose'] as bool) {
Logger.root.level = Level.ALL;
}
return super.runCommand(topLevelResults);
}
}
abstract class KdbxFileCommand extends Command<void> {
KdbxFileCommand() {
argParser.addOption(
'input',
abbr: 'i',
help: 'Input kdbx file',
valueHelp: 'foo.kdbx',
);
}
@override
FutureOr<void> run() async {
final inputFile = argResults['input'] as String;
if (inputFile == null) {
usageException('Required argument: --input');
}
final bytes = await File(inputFile).readAsBytes();
final password = prompts.get('Password for $inputFile', conceal: true, validate: (str) => str.isNotEmpty);
final file = await KdbxFormat.read(bytes, Credentials(ProtectedValue.fromString(password)));
return runWithFile(file);
}
Future<void> runWithFile(KdbxFile file);
}
class CatCommand extends KdbxFileCommand {
@override
String get description => 'outputs all entries from file.';
@override
String get name => 'cat';
@override
Future<void> runWithFile(KdbxFile file) async {
_logger.severe('running');
}
}

25
lib/src/crypto/protected_salt_generator.dart

@ -0,0 +1,25 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:pointycastle/export.dart';
class ProtectedSaltGenerator {
factory ProtectedSaltGenerator(Uint8List key) {
final hash = sha256.convert(key).bytes as Uint8List;
final cipher = Salsa20Engine()..init(false, ParametersWithIV(KeyParameter(hash), SalsaNonce));
return ProtectedSaltGenerator._(cipher);
}
ProtectedSaltGenerator._(this.cipher);
static final SalsaNonce = Uint8List.fromList([0xE8, 0x30, 0x09, 0x4B, 0x97, 0x20, 0x5D, 0x2A]);
final StreamCipher cipher;
String decryptBase64(String protectedValue) {
final bytes = base64.decode(protectedValue);
final result = cipher.process(bytes);
final decrypted = utf8.decode(result);
return decrypted;
}
}

4
lib/src/internal/crypto_utils.dart

@ -4,10 +4,6 @@ import 'dart:typed_data';
import 'package:pointycastle/export.dart';
class CryptoUtils {
}
/// https://gist.github.com/proteye/e54eef1713e1fe9123d1eb04c0a5cf9b
class AesHelper {
static const CBC_MODE = 'CBC';

5
lib/src/kdbx_entry.dart

@ -0,0 +1,5 @@
class KdbxEntry {
}

97
lib/src/kdbx_format.dart

@ -3,23 +3,48 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/src/crypto/protected_salt_generator.dart';
import 'package:kdbx/src/crypto/protected_value.dart';
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';
import 'package:xml/xml.dart' as xml;
final _logger = Logger('kdbx.format');
class KdbxFile {
KdbxFile(this.credentials, this.header, this.body);
static final protectedValues = Expando<ProtectedValue>();
final Credentials credentials;
final KdbxHeader header;
final KdbxBody body;
}
class KdbxBody {
KdbxBody(this.xmlDocument, this.meta);
final xml.XmlDocument xmlDocument;
final KdbxMeta meta;
}
class KdbxMeta {
}
class KdbxFormat {
static Future<void> read(Uint8List input, Credentials credentials) async {
static Future<KdbxFile> read(Uint8List input, Credentials credentials) async {
final reader = ReaderHelper(input);
final header = await KdbxHeader.read(reader);
_loadV3(header, reader, credentials);
return _loadV3(header, reader, credentials);
}
static void _loadV3(KdbxHeader header, ReaderHelper reader, Credentials credentials) {
static KdbxFile _loadV3(
KdbxHeader header, ReaderHelper reader, Credentials credentials) {
// _getMasterKeyV3(header, credentials);
final masterKey = _generateMasterKeyV3(header, credentials);
final encryptedPayload = reader.readRemaining();
@ -30,54 +55,80 @@ class KdbxFormat {
if (header.compression == Compression.gzip) {
final xml = GZipCodec().decode(blocks);
final string = utf8.decode(xml);
print('xml: $string');
return KdbxFile(credentials, header, _loadXml(header, string));
} else {
return KdbxFile(credentials, header, _loadXml(header, utf8.decode(blocks)));
}
}
static KdbxBody _loadXml(KdbxHeader header, String xmlString) {
final protectedValueEncryption = header.innerRandomStreamEncryption;
if (protectedValueEncryption != PotectedValueEncryption.salsa20) {
throw KdbxUnsupportedException(
'Inner encryption: $protectedValueEncryption');
}
final streamKey =
header.fields[HeaderFields.ProtectedStreamKey].bytes.asUint8List();
final gen = ProtectedSaltGenerator(streamKey);
final document = xml.parse(xmlString);
// 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)');
for (final el in document
.findAllElements('Value')
.where((el) => el.getAttribute('Protected')?.toLowerCase() == 'true')) {
final pw = gen.decryptBase64(el.text.trim());
KdbxFile.protectedValues[el] = ProtectedValue.fromString(pw);
}
// final result = AesHelper.decrypt(masterKey, reader.readRemaining());
// print('before : ${_toHexList(encryptedPayload)}');
final keePassFile = document.findElements('KeePassFile').single;
final meta = keePassFile.findElements('Meta').single;
final groupRoot = keePassFile.findElements('Root').single;
_logger.fine('got meta: ${meta.toXmlString(pretty: true)}');
return KdbxBody(document, KdbxMeta());
}
static Uint8List _decryptContent(KdbxHeader header, Uint8List masterKey, Uint8List 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()));
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))}');
_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))) {
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;
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 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;
final masterKey = crypto.sha256
.convert(Uint8List.fromList(masterSeed.asUint8List() + transformedKey))
.bytes as Uint8List;
return masterKey;
}
}

4
lib/src/kdbx_group.dart

@ -0,0 +1,4 @@
class KdbxGroup {
}

16
lib/src/kdbx_header.dart

@ -1,5 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:convert/convert.dart' as convert;
@ -7,7 +5,6 @@ import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/internal/byte_utils.dart';
import 'package:logging/logging.dart';
import 'package:pointycastle/export.dart';
final _logger = Logger('kdbx.header');
@ -25,6 +22,9 @@ enum Compression {
gzip,
}
/// how protected values are encrypted in the xml.
enum PotectedValueEncryption { plainText, arc4variant, salsa20 }
enum HeaderFields {
EndOfHeader,
Comment,
@ -79,9 +79,9 @@ class KdbxHeader {
static Iterable<HeaderField> readField(ReaderHelper reader, int versionMajor) sync* {
while (true) {
final headerId = reader.readUint8();
int size = versionMajor >= 4 ? reader.readUint32() : reader.readUint16();
final int bodySize = versionMajor >= 4 ? reader.readUint32() : reader.readUint16();
_logger.finer('Read header ${HeaderFields.values[headerId]}');
final bodyBytes = size > 0 ? reader.readBytes(size) : null;
final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null;
if (headerId > 0) {
yield HeaderField(HeaderFields.values[headerId], bodyBytes);
} else {
@ -106,6 +106,9 @@ class KdbxHeader {
throw KdbxUnsupportedException('compression');
}
}
PotectedValueEncryption get innerRandomStreamEncryption =>
PotectedValueEncryption.values[fields[HeaderFields.InnerRandomStreamID].bytes.asUint32List().single];
}
class Credentials {
@ -134,8 +137,6 @@ class KdbxUnsupportedException implements KdbxException {
final String hint;
}
class HashedBlockReader {
static Uint8List readBlocks(ReaderHelper reader) =>
Uint8List.fromList(readNextBlock(reader).expand((x) => x).toList());
@ -176,4 +177,3 @@ class ReaderHelper {
Uint8List readRemaining() => data.sublist(pos);
}

10
pubspec.yaml

@ -11,10 +11,14 @@ dependencies:
# path: ^1.6.0
logging: '>=0.11.3+2 <1.0.0'
crypto: '>=2.0.0 <3.0.0'
pointycastle: ^1.0.1
xml: ^3.5.0
pointycastle: '>=1.0.1 <2.0.0'
xml: '>=3.5.0 <4.0.0'
dev_dependencies:
# required for bin/
args: '>1.5.0 <2.0.0'
prompts: '>=1.3.0 <2.0.0'
logging_appenders: '>=0.1.0 <1.0.0'
dev_dependencies:
pedantic: ^1.7.0
test: ^1.6.0

122
test/FooBar.2.content.xml

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<KeePassFile>
<Meta>
<Generator>KdbxWeb</Generator>
<HeaderHash>r9uD96EiudcSeXq62ma/OqtBBPf+EYX7iMrBaNJBRwo=</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>
<Entry>
<UUID>RMUeiV24MDcrIY8qQYRAlg==</UUID>
<IconID>0</IconID>
<ForegroundColor />
<BackgroundColor />
<OverrideURL />
<Tags />
<Times>
<CreationTime>2019-08-21T00:17:25Z</CreationTime>
<LastModificationTime>2019-08-21T00:17:36Z</LastModificationTime>
<LastAccessTime>2019-08-21T00:17:36Z</LastAccessTime>
<ExpiryTime>2019-08-21T00:17:25Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>2019-08-21T00:17:25Z</LocationChanged>
</Times>
<String>
<Key>Title</Key>
<Value>loremipsum.com</Value>
</String>
<String>
<Key>UserName</Key>
<Value>foo</Value>
</String>
<String>
<Key>Password</Key>
<Value Protected="True">QcvZ</Value>
</String>
<String>
<Key>URL</Key>
<Value />
</String>
<String>
<Key>Notes</Key>
<Value />
</String>
<AutoType>
<Enabled>True</Enabled>
<DataTransferObfuscation>0</DataTransferObfuscation>
</AutoType>
<History />
</Entry>
</Group>
<DeletedObjects />
</Root>
</KeePassFile>
Loading…
Cancel
Save