diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 795f935..c221ce3 100644
--- a/README.md
+++ b/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
diff --git a/bin/kdbx.dart b/bin/kdbx.dart
new file mode 100644
index 0000000..2a12b9a
--- /dev/null
+++ b/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 arguments) {
+ exitCode = 0;
+
+ final runner = KdbxCommandRunner('kdbx', 'Kdbx Utility');
+ runner.run(arguments).catchError((dynamic error, StackTrace stackTrace) {
+ if (error is! UsageException) {
+ return Future.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 {
+ KdbxCommandRunner(String executableName, String description) : super(executableName, description) {
+ argParser.addFlag('verbose', abbr: 'v');
+ addCommand(CatCommand());
+ }
+
+ @override
+ Future 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 {
+ KdbxFileCommand() {
+ argParser.addOption(
+ 'input',
+ abbr: 'i',
+ help: 'Input kdbx file',
+ valueHelp: 'foo.kdbx',
+ );
+ }
+
+ @override
+ FutureOr 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 runWithFile(KdbxFile file);
+}
+
+class CatCommand extends KdbxFileCommand {
+ @override
+ String get description => 'outputs all entries from file.';
+
+ @override
+ String get name => 'cat';
+
+ @override
+ Future runWithFile(KdbxFile file) async {
+ _logger.severe('running');
+ }
+}
diff --git a/lib/src/crypto/protected_salt_generator.dart b/lib/src/crypto/protected_salt_generator.dart
new file mode 100644
index 0000000..265f8a7
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/lib/src/internal/crypto_utils.dart b/lib/src/internal/crypto_utils.dart
index f0e2fc6..5e79bb1 100644
--- a/lib/src/internal/crypto_utils.dart
+++ b/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';
diff --git a/lib/src/kdbx_entry.dart b/lib/src/kdbx_entry.dart
new file mode 100644
index 0000000..249a875
--- /dev/null
+++ b/lib/src/kdbx_entry.dart
@@ -0,0 +1,5 @@
+
+
+class KdbxEntry {
+
+}
\ No newline at end of file
diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart
index 9ec1b5c..657f0f6 100644
--- a/lib/src/kdbx_format.dart
+++ b/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();
+
+ 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 read(Uint8List input, Credentials credentials) async {
+
+ static Future 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;
}
-
}
diff --git a/lib/src/kdbx_group.dart b/lib/src/kdbx_group.dart
new file mode 100644
index 0000000..01fb46a
--- /dev/null
+++ b/lib/src/kdbx_group.dart
@@ -0,0 +1,4 @@
+
+class KdbxGroup {
+
+}
\ No newline at end of file
diff --git a/lib/src/kdbx_header.dart b/lib/src/kdbx_header.dart
index 0c49e05..b883e97 100644
--- a/lib/src/kdbx_header.dart
+++ b/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 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);
}
-
diff --git a/pubspec.yaml b/pubspec.yaml
index 3af2f8a..fe5a52e 100644
--- a/pubspec.yaml
+++ b/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
diff --git a/test/FooBar.2.content.xml b/test/FooBar.2.content.xml
new file mode 100644
index 0000000..6d3ecd8
--- /dev/null
+++ b/test/FooBar.2.content.xml
@@ -0,0 +1,122 @@
+
+
+
+ KdbxWeb
+ r9uD96EiudcSeXq62ma/OqtBBPf+EYX7iMrBaNJBRwo=
+ FooBar
+ 2019-08-20T13:16:06Z
+
+ 2019-08-20T13:15:47Z
+
+ 2019-08-20T13:15:47Z
+ 365
+
+ 2019-08-20T13:16:03Z
+ -1
+ -1
+ True
+ dVSBC/BAx70qcsy6XkrGJA==
+ 2019-08-20T13:15:47Z
+ AAAAAAAAAAAAAAAAAAAAAA==
+ 2019-08-20T13:15:47Z
+ 10
+ 6291456
+
+
+
+ False
+ False
+ True
+ False
+ False
+
+
+
+
+
+
+
+ LAQMkihXTkxhA2D2tE40Fg==
+ FooBar
+
+ 49
+
+ 2019-08-20T13:15:47Z
+ 2019-08-20T13:16:06Z
+ 2019-08-20T13:16:06Z
+ 2019-08-20T13:15:47Z
+ False
+ 0
+ 2019-08-20T13:15:47Z
+
+ True
+
+ null
+ null
+ AAAAAAAAAAAAAAAAAAAAAA==
+
+ dVSBC/BAx70qcsy6XkrGJA==
+ Recycle Bin
+
+ 43
+
+ 2019-08-20T13:15:47Z
+ 2019-08-20T13:15:47Z
+ 2019-08-20T13:15:47Z
+ 2019-08-20T13:15:47Z
+ False
+ 0
+ 2019-08-20T13:15:47Z
+
+ True
+
+ False
+ False
+ AAAAAAAAAAAAAAAAAAAAAA==
+
+
+ RMUeiV24MDcrIY8qQYRAlg==
+ 0
+
+
+
+
+
+ 2019-08-21T00:17:25Z
+ 2019-08-21T00:17:36Z
+ 2019-08-21T00:17:36Z
+ 2019-08-21T00:17:25Z
+ False
+ 0
+ 2019-08-21T00:17:25Z
+
+
+ Title
+ loremipsum.com
+
+
+ UserName
+ foo
+
+
+ Password
+ QcvZ
+
+
+ URL
+
+
+
+ Notes
+
+
+
+ True
+ 0
+
+
+
+
+
+
+
\ No newline at end of file