KeepassX format implementation in pure dart.
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.

245 lines
5.8 KiB

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';
}
}