|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:typed_data';
|
|
|
|
|
|
|
|
import 'package:clock/clock.dart';
|
|
|
|
import 'package:kdbx/kdbx.dart';
|
|
|
|
import 'package:kdbx/src/internal/byte_utils.dart';
|
|
|
|
import 'package:kdbx/src/kdbx_consts.dart';
|
|
|
|
import 'package:meta/meta.dart';
|
|
|
|
import 'package:xml/xml.dart';
|
|
|
|
|
|
|
|
class KdbxXml {
|
|
|
|
static const NODE_STRING = 'String';
|
|
|
|
static const NODE_KEY = 'Key';
|
|
|
|
static const NODE_VALUE = 'Value';
|
|
|
|
static const ATTR_PROTECTED = 'Protected';
|
|
|
|
static const ATTR_COMPRESSED = 'Compressed';
|
|
|
|
static const NODE_HISTORY = 'History';
|
|
|
|
static const NODE_BINARIES = 'Binaries';
|
|
|
|
static const ATTR_ID = 'ID';
|
|
|
|
static const NODE_BINARY = 'Binary';
|
|
|
|
static const ATTR_REF = 'Ref';
|
|
|
|
|
|
|
|
static const NODE_CUSTOM_DATA_ITEM = 'Item';
|
|
|
|
}
|
|
|
|
|
|
|
|
extension XmlElementKdbx on XmlElement {
|
|
|
|
bool getAttributeBool(String name) =>
|
|
|
|
getAttribute(name)?.toLowerCase() == 'true';
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract class KdbxSubNode<T> {
|
|
|
|
KdbxSubNode(this.node, this.name);
|
|
|
|
|
|
|
|
final KdbxNode node;
|
|
|
|
final String name;
|
|
|
|
|
|
|
|
T get();
|
|
|
|
|
|
|
|
void set(T value);
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract class KdbxSubTextNode<T> extends KdbxSubNode<T> {
|
|
|
|
KdbxSubTextNode(KdbxNode node, String name) : super(node, name);
|
|
|
|
|
|
|
|
@protected
|
|
|
|
String encode(T value);
|
|
|
|
|
|
|
|
@protected
|
|
|
|
T decode(String value);
|
|
|
|
|
|
|
|
XmlElement _opt(String nodeName) => node.node
|
|
|
|
.findElements(nodeName)
|
|
|
|
.singleWhere((x) => true, orElse: () => null);
|
|
|
|
|
|
|
|
@override
|
|
|
|
T get() {
|
|
|
|
final textValue = _opt(name)?.text;
|
|
|
|
if (textValue == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return decode(textValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void set(T value) {
|
|
|
|
node.isDirty = true;
|
|
|
|
final el =
|
|
|
|
node.node.findElements(name).singleWhere((x) => true, orElse: () {
|
|
|
|
final el = XmlElement(XmlName(name));
|
|
|
|
node.node.children.add(el);
|
|
|
|
return el;
|
|
|
|
});
|
|
|
|
el.children.clear();
|
|
|
|
if (value == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
final stringValue = encode(value);
|
|
|
|
if (stringValue == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
el.children.add(XmlText(stringValue));
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
String toString() {
|
|
|
|
return '$runtimeType{${_opt(name)?.text}}';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class IntNode extends KdbxSubTextNode<int> {
|
|
|
|
IntNode(KdbxNode node, String name) : super(node, name);
|
|
|
|
|
|
|
|
@override
|
|
|
|
int decode(String value) => int.tryParse(value);
|
|
|
|
|
|
|
|
@override
|
|
|
|
String encode(int value) => value.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
class StringNode extends KdbxSubTextNode<String> {
|
|
|
|
StringNode(KdbxNode node, String name) : super(node, name);
|
|
|
|
|
|
|
|
@override
|
|
|
|
String decode(String value) => value;
|
|
|
|
|
|
|
|
@override
|
|
|
|
String encode(String value) => value;
|
|
|
|
}
|
|
|
|
|
|
|
|
class Base64Node extends KdbxSubTextNode<ByteBuffer> {
|
|
|
|
Base64Node(KdbxNode node, String name) : super(node, name);
|
|
|
|
|
|
|
|
@override
|
|
|
|
ByteBuffer decode(String value) => base64.decode(value).buffer;
|
|
|
|
|
|
|
|
@override
|
|
|
|
String encode(ByteBuffer value) => base64.encode(value.asUint8List());
|
|
|
|
}
|
|
|
|
|
|
|
|
class UuidNode extends KdbxSubTextNode<KdbxUuid> {
|
|
|
|
UuidNode(KdbxNode node, String name) : super(node, name);
|
|
|
|
|
|
|
|
@override
|
|
|
|
KdbxUuid decode(String value) => KdbxUuid(value);
|
|
|
|
|
|
|
|
@override
|
|
|
|
String encode(KdbxUuid value) => value.uuid;
|
|
|
|
}
|
|
|
|
|
|
|
|
class IconNode extends KdbxSubTextNode<KdbxIcon> {
|
|
|
|
IconNode(KdbxNode node, String name) : super(node, name);
|
|
|
|
|
|
|
|
@override
|
|
|
|
KdbxIcon decode(String value) => KdbxIcon.values[int.tryParse(value)];
|
|
|
|
|
|
|
|
@override
|
|
|
|
String encode(KdbxIcon value) => value.index.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
class BooleanNode extends KdbxSubTextNode<bool> {
|
|
|
|
BooleanNode(KdbxNode node, String name) : super(node, name);
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool decode(String value) {
|
|
|
|
switch (value?.toLowerCase()) {
|
|
|
|
case 'null':
|
|
|
|
return null;
|
|
|
|
case 'true':
|
|
|
|
return true;
|
|
|
|
case 'false':
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
throw KdbxCorruptedFileException('Invalid boolean value $value for $name');
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
String encode(bool value) => value ? 'true' : 'false';
|
|
|
|
}
|
|
|
|
|
|
|
|
class DateTimeUtcNode extends KdbxSubTextNode<DateTime> {
|
|
|
|
DateTimeUtcNode(KdbxNode node, String name) : super(node, name);
|
|
|
|
|
|
|
|
void setToNow() {
|
|
|
|
set(clock.now().toUtc());
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
DateTime decode(String value) {
|
|
|
|
if (value == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if (value.contains(':')) {
|
|
|
|
return DateTime.parse(value);
|
|
|
|
}
|
|
|
|
// kdbx 4.x uses base64 encoded date.
|
|
|
|
final decoded = base64.decode(value);
|
|
|
|
const EpochSeconds = 62135596800;
|
|
|
|
|
|
|
|
final secondsFrom00 = ReaderHelper(decoded).readUint64();
|
|
|
|
|
|
|
|
return DateTime.fromMillisecondsSinceEpoch(
|
|
|
|
(secondsFrom00 - EpochSeconds) * 1000,
|
|
|
|
isUtc: true);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
String encode(DateTime value) {
|
|
|
|
assert(value.isUtc);
|
|
|
|
|
|
|
|
// TODO for kdbx v4 we need to support binary/base64
|
|
|
|
return DateTimeUtils.toIso8601StringSeconds(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class XmlUtils {
|
|
|
|
static void removeChildrenByName(XmlNode node, String name) {
|
|
|
|
node.children
|
|
|
|
.removeWhere((node) => node is XmlElement && node.name.local == name);
|
|
|
|
}
|
|
|
|
|
|
|
|
static XmlElement createTextNode(String localName, String value) =>
|
|
|
|
createNode(localName, [XmlText(value)]);
|
|
|
|
|
|
|
|
static XmlElement createNode(
|
|
|
|
String localName, [
|
|
|
|
List<XmlNode> children = const [],
|
|
|
|
]) =>
|
|
|
|
XmlElement(XmlName(localName))..children.addAll(children);
|
|
|
|
}
|
|
|
|
|
|
|
|
class DateTimeUtils {
|
|
|
|
static String toIso8601StringSeconds(DateTime dateTime) {
|
|
|
|
final y = _fourDigits(dateTime.year);
|
|
|
|
final m = _twoDigits(dateTime.month);
|
|
|
|
final d = _twoDigits(dateTime.hour);
|
|
|
|
final h = _twoDigits(dateTime.hour);
|
|
|
|
final min = _twoDigits(dateTime.minute);
|
|
|
|
final sec = _twoDigits(dateTime.second);
|
|
|
|
return '$y-$m-${d}T$h:$min:${sec}Z';
|
|
|
|
}
|
|
|
|
|
|
|
|
static String _fourDigits(int n) {
|
|
|
|
final absN = n.abs();
|
|
|
|
final sign = n < 0 ? '-' : '';
|
|
|
|
// ignore: prefer_single_quotes
|
|
|
|
if (absN >= 1000) {
|
|
|
|
return '$n';
|
|
|
|
}
|
|
|
|
if (absN >= 100) {
|
|
|
|
return '${sign}0$absN';
|
|
|
|
}
|
|
|
|
if (absN >= 10) {
|
|
|
|
return '${sign}00$absN';
|
|
|
|
}
|
|
|
|
return '${sign}000$absN';
|
|
|
|
}
|
|
|
|
|
|
|
|
static String _twoDigits(int n) {
|
|
|
|
if (n >= 10) {
|
|
|
|
return '$n';
|
|
|
|
}
|
|
|
|
return '0$n';
|
|
|
|
}
|
|
|
|
}
|