You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
166 lines
4.6 KiB
166 lines
4.6 KiB
import 'dart:async'; |
|
import 'dart:io'; |
|
|
|
import 'package:argon2_ffi_base/argon2_ffi_base.dart'; |
|
import 'package:args/args.dart'; |
|
import 'package:args/command_runner.dart'; |
|
import 'package:kdbx/kdbx.dart'; |
|
import 'package:kdbx/src/crypto/protected_value.dart'; |
|
import 'package:kdbx/src/kdbx_format.dart'; |
|
import 'package:kdbx/src/kdbx_group.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); |
|
}); |
|
|
|
// 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()); |
|
addCommand(DumpXmlCommand()); |
|
} |
|
|
|
@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', |
|
); |
|
argParser.addOption( |
|
'keyfile', |
|
abbr: 'k', |
|
help: 'Keyfile for decryption', |
|
); |
|
argParser.addOption( |
|
'password', |
|
abbr: 'p', |
|
help: 'password', |
|
valueHelp: 'asdf', |
|
); |
|
} |
|
|
|
@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 = argResults['password'] as String ?? |
|
prompts.get('Password for $inputFile', |
|
conceal: true, validate: (str) => str.isNotEmpty); |
|
final keyFile = argResults['keyfile'] as String; |
|
final keyFileData = |
|
keyFile == null ? null : await File(keyFile).readAsBytes(); |
|
|
|
Argon2.resolveLibraryForceDynamic = true; |
|
final file = await KdbxFormat(Argon2FfiFlutter()).read( |
|
bytes, |
|
Credentials.composite(ProtectedValue.fromString(password), keyFileData), |
|
); |
|
return runWithFile(file); |
|
} |
|
|
|
Future<void> runWithFile(KdbxFile file); |
|
} |
|
|
|
class CatCommand extends KdbxFileCommand { |
|
CatCommand() { |
|
argParser.addFlag('decrypt', |
|
help: 'Force decryption of all protected strings.'); |
|
argParser.addFlag('all-fields', |
|
help: 'Force decryption of all protected strings.'); |
|
} |
|
|
|
@override |
|
String get description => 'outputs all entries from file.'; |
|
|
|
@override |
|
String get name => 'cat'; |
|
|
|
bool get forceDecrypt => argResults['decrypt'] as bool; |
|
|
|
bool get allFields => argResults['all-fields'] as bool; |
|
|
|
@override |
|
Future<void> runWithFile(KdbxFile file) async { |
|
catGroup(file.body.rootGroup); |
|
} |
|
|
|
void catGroup(KdbxGroup group, {int depth = 0}) { |
|
final indent = ' ' * depth; |
|
print('$indent + ${group.name} (${group.uuid})'); |
|
for (final group in group.groups) { |
|
catGroup(group, depth: depth + 1); |
|
} |
|
final valueToSting = (StringValue value) => |
|
forceDecrypt ? value?.getText() : value?.toString(); |
|
|
|
for (final entry in group.entries) { |
|
final value = entry.getString(KdbxKey('Password')); |
|
print('$indent `- ${entry.getString(KdbxKey('Title'))?.getText()}: ' |
|
'${valueToSting(value)}'); |
|
if (allFields) { |
|
print(entry.stringEntries |
|
.map((field) => |
|
'$indent ${field.key} = ${valueToSting(field.value)}') |
|
.join('\n')); |
|
} |
|
print(entry.binaryEntries |
|
.map((b) => '$indent `- file: ${b.key} - ${b.value.value.length}') |
|
.join('\n')); |
|
} |
|
} |
|
} |
|
|
|
class DumpXmlCommand extends KdbxFileCommand { |
|
@override |
|
String get description => 'Outputs the xml body unencrypted.'; |
|
|
|
@override |
|
String get name => 'dumpXml'; |
|
|
|
@override |
|
List<String> get aliases => ['dump', 'xml']; |
|
|
|
@override |
|
Future<void> runWithFile(KdbxFile file) async { |
|
print(file.body.node.toXmlString(pretty: true)); |
|
} |
|
}
|
|
|