@hazae41/kdbx
Version:
Rust-like KeePass (KDBX 4) file format for TypeScript
826 lines (825 loc) • 32.8 kB
JavaScript
// deno-lint-ignore-file no-namespace
import { BytesAsUuid, StringAsUuid } from "../../../../../libs/uuid/mod.js";
import { Cursor } from "@hazae41/cursor";
export class KeePassFile {
document;
constructor(document) {
this.document = document;
}
static decodeOrThrow(bytes) {
return new KeePassFile(new DOMParser().parseFromString(new TextDecoder().decode(bytes), "text/xml"));
}
encodeOrThrow() {
return new TextEncoder().encode(new XMLSerializer().serializeToString(this.document));
}
cloneOrThrow() {
return KeePassFile.decodeOrThrow(this.encodeOrThrow());
}
getMetaOrThrow() {
const element = this.document.querySelector(":scope > Meta");
if (element == null)
throw new Error();
return new KeePassFile.Meta(element);
}
getRootOrThrow() {
const element = this.document.querySelector(":scope > Root");
if (element == null)
throw new Error();
return new KeePassFile.Root(element);
}
}
(function (KeePassFile) {
class Meta {
element;
constructor(element) {
this.element = element;
}
getDatabaseNameOrNull() {
const element = this.element.querySelector(":scope > DatabaseName");
if (element == null)
return;
return new Other.AsString(element);
}
getDatabaseNameChangedOrNull() {
const stale = this.element.querySelector(":scope > DatabaseNameChanged");
if (stale == null)
return;
return new Other.AsDate(stale);
}
setDatabaseNameChanged(date = new Date()) {
const stale = this.element.querySelector(":scope > DatabaseNameChanged");
if (stale != null) {
const value = new Other.AsDate(stale);
value.setOrThrow(date);
return;
}
const fresh = this.element.ownerDocument.createElement("DatabaseNameChanged");
this.element.appendChild(fresh);
const value = new Other.AsDate(fresh);
value.setOrThrow(date);
}
getGeneratorOrNull() {
const element = this.element.querySelector(":scope > Generator");
if (element == null)
return;
return new Other.AsString(element);
}
getHistoryMaxItemsOrNull() {
const element = this.element.querySelector(":scope > HistoryMaxItems");
if (element == null)
return;
return new Other.AsInteger(element);
}
getHistoryMaxSizeOrNull() {
const element = this.element.querySelector(":scope > HistoryMaxSize");
if (element == null)
return;
return new Other.AsInteger(element);
}
getRecycleBinEnabledOrNull() {
const element = this.element.querySelector(":scope > RecycleBinEnabled");
if (element == null)
return;
return new Other.AsBoolean(element);
}
getRecycleBinUuidOrThrow() {
const element = this.element.querySelector(":scope > RecycleBinUUID");
if (element == null)
throw new Error();
return new Other.AsUuid(element);
}
getRecycleBinChangedOrNull() {
const element = this.element.querySelector(":scope > RecycleBinChanged");
if (element == null)
return;
return new Other.AsDate(element);
}
setRecycleBinChanged(date = new Date()) {
const stale = this.element.querySelector(":scope > RecycleBinChanged");
if (stale != null) {
const value = new Other.AsDate(stale);
value.setOrThrow(date);
return;
}
const fresh = this.element.ownerDocument.createElement("RecycleBinChanged");
this.element.appendChild(fresh);
const value = new Other.AsDate(fresh);
value.setOrThrow(date);
}
getSettingsChangedOrNull() {
const element = this.element.querySelector(":scope > SettingsChanged");
if (element == null)
return;
return new Other.AsDate(element);
}
setSettingsChanged(date = new Date()) {
const stale = this.element.querySelector(":scope > SettingsChanged");
if (stale != null) {
const value = new Other.AsDate(stale);
value.setOrThrow(date);
return;
}
const fresh = this.element.ownerDocument.createElement("SettingsChanged");
this.element.appendChild(fresh);
const value = new Other.AsDate(fresh);
value.setOrThrow(date);
}
getDatabaseDescriptionOrNull() {
const element = this.element.querySelector(":scope > DatabaseDescription");
if (element == null)
return;
return new Other.AsString(element);
}
getDatabaseDescriptionChangedOrNull() {
const element = this.element.querySelector(":scope > DatabaseDescriptionChanged");
if (element == null)
return;
return new Other.AsDate(element);
}
setDatabaseDescriptionChanged(date = new Date()) {
const stale = this.element.querySelector(":scope > DatabaseDescriptionChanged");
if (stale != null) {
const value = new Other.AsDate(stale);
value.setOrThrow(date);
return;
}
const fresh = this.element.ownerDocument.createElement("DatabaseDescriptionChanged");
this.element.appendChild(fresh);
const value = new Other.AsDate(fresh);
value.setOrThrow(date);
}
getDefaultUserNameOrNull() {
const element = this.element.querySelector(":scope > DefaultUserName");
if (element == null)
return;
return new Other.AsString(element);
}
getDefaultUserNameChangedOrNull() {
const element = this.element.querySelector(":scope > DefaultUserNameChanged");
if (element == null)
return;
return new Other.AsDate(element);
}
setDefaultUserNameChanged(date = new Date()) {
const stale = this.element.querySelector(":scope > DefaultUserNameChanged");
if (stale != null) {
const value = new Other.AsDate(stale);
value.setOrThrow(date);
return;
}
const fresh = this.element.ownerDocument.createElement("DefaultUserNameChanged");
this.element.appendChild(fresh);
const value = new Other.AsDate(fresh);
value.setOrThrow(date);
}
getColorOrNull() {
const element = this.element.querySelector(":scope > Color");
if (element == null)
return;
return new Other.AsString(element);
}
getEntryTemplatesGroupOrNull() {
const element = this.element.querySelector(":scope > EntryTemplatesGroup");
if (element == null)
return;
return new Other.AsString(element);
}
getEntryTemplatesGroupChangedOrNull() {
const element = this.element.querySelector(":scope > EntryTemplatesGroupChanged");
if (element == null)
return;
return new Other.AsDate(element);
}
setEntryTemplatesGroupChanged(date = new Date()) {
const stale = this.element.querySelector(":scope > EntryTemplatesGroupChanged");
if (stale != null) {
const value = new Other.AsDate(stale);
value.setOrThrow(date);
return;
}
const fresh = this.element.ownerDocument.createElement("EntryTemplatesGroupChanged");
this.element.appendChild(fresh);
const value = new Other.AsDate(fresh);
value.setOrThrow(date);
}
}
KeePassFile.Meta = Meta;
class Root {
element;
constructor(element) {
this.element = element;
}
addGroupOrThrow(name) {
const $group = new KeePassFile.Group(this.element.ownerDocument.createElement("Group"));
{
const $name = new KeePassFile.Other.AsString(this.element.ownerDocument.createElement("Name"));
const $uuid = new KeePassFile.Other.AsUuid(this.element.ownerDocument.createElement("UUID"));
const $times = new KeePassFile.Times(this.element.ownerDocument.createElement("Times"));
$name.set(name);
$uuid.setOrThrow(crypto.randomUUID());
{
const $creationTime = new KeePassFile.Other.AsDate(this.element.ownerDocument.createElement("CreationTime"));
$creationTime.setOrThrow(new Date());
$times.element.appendChild($creationTime.element);
}
$group.element.appendChild($name.element);
$group.element.appendChild($uuid.element);
$group.element.appendChild($times.element);
}
this.element.appendChild($group.element);
return $group;
}
*getGroups() {
const elements = this.element.querySelectorAll(`Group`);
for (const element of elements)
yield new Group(element);
return;
}
getGroupByUuidOrThrow(uuid) {
const elements = this.element.querySelectorAll(`Group`);
for (const element of elements) {
const group = new Group(element);
if (group.getUuidOrThrow().getOrThrow() === uuid)
return group;
continue;
}
throw new Error();
}
getGroupByUuidOrNull(uuid) {
const elements = this.element.querySelectorAll(`Group`);
for (const element of elements) {
const group = new Group(element);
if (group.getUuidOrThrow().getOrThrow() === uuid)
return group;
continue;
}
return;
}
*getDirectGroups() {
const elements = this.element.querySelectorAll(`:scope > Group`);
for (const element of elements)
yield new Group(element);
return;
}
getDirectGroupByIndexOrThrow(index) {
const element = this.element.querySelector(`:scope > Group:nth-of-type(${index + 1})`);
if (element == null)
throw new Error();
return new Group(element);
}
getDirectGroupByIndexOrNull(index) {
const element = this.element.querySelector(`:scope > Group:nth-of-type(${index + 1})`);
if (element == null)
return;
return new Group(element);
}
getDirectGroupByUuidOrThrow(uuid) {
const elements = this.element.querySelectorAll(`:scope > Group`);
for (const element of elements) {
const group = new Group(element);
if (group.getUuidOrThrow().getOrThrow() === uuid)
return group;
continue;
}
throw new Error();
}
getDirectGroupByUuidOrNull(uuid) {
const elements = this.element.querySelectorAll(`:scope > Group`);
for (const element of elements) {
const group = new Group(element);
if (group.getUuidOrThrow().getOrThrow() === uuid)
return group;
continue;
}
return;
}
}
KeePassFile.Root = Root;
class Group {
element;
constructor(element) {
this.element = element;
}
addEntryOrThrow() {
const $entry = new KeePassFile.Entry(this.element.ownerDocument.createElement("Entry"));
{
const $uuid = new KeePassFile.Other.AsUuid(this.element.ownerDocument.createElement("UUID"));
const $times = new KeePassFile.Times(this.element.ownerDocument.createElement("Times"));
$uuid.setOrThrow(crypto.randomUUID());
{
const $creationTime = new KeePassFile.Other.AsDate(this.element.ownerDocument.createElement("CreationTime"));
$creationTime.setOrThrow(new Date());
$times.element.appendChild($creationTime.element);
}
$entry.element.appendChild($uuid.element);
$entry.element.appendChild($times.element);
}
this.element.appendChild($entry.element);
return $entry;
}
moveOrThrow(group) {
if (this.element.parentNode === group.element)
return;
this.element.parentNode?.removeChild(this.element);
group.element.appendChild(this.element);
this.getTimesOrNew().setLocationChanged();
}
getNameOrThrow() {
const element = this.element.querySelector(":scope > Name");
if (element == null)
throw new Error();
return new Other.AsString(element);
}
getUuidOrThrow() {
const element = this.element.querySelector(":scope > UUID");
if (element == null)
throw new Error();
return new Other.AsUuid(element);
}
getTimesOrNew() {
const stale = this.element.querySelector(":scope > Times");
if (stale != null)
return new Times(stale);
const $times = new Times(this.element.ownerDocument.createElement("Times"));
{
const $creationTime = new Other.AsDate(this.element.ownerDocument.createElement("CreationTime"));
$creationTime.setOrThrow(new Date());
$times.element.appendChild($creationTime.element);
}
this.element.appendChild($times.element);
return $times;
}
getIconIdOrThrow() {
const element = this.element.querySelector(":scope > IconID");
if (element == null)
throw new Error();
return new Other.AsInteger(element);
}
getEnableAutoTypeOrThrow() {
const element = this.element.querySelector(":scope > EnableAutoType");
if (element == null)
throw new Error();
return new Other.AsBoolean(element);
}
getEnableSearchingOrThrow() {
const element = this.element.querySelector(":scope > EnableSearching");
if (element == null)
throw new Error();
return new Other.AsBoolean(element);
}
*getDirectGroups() {
const elements = this.element.querySelectorAll(`:scope > Group`);
for (const element of elements)
yield new Group(element);
return;
}
getDirectGroupByIndexOrThrow(index) {
const element = this.element.querySelector(`:scope > Group:nth-of-type(${index + 1})`);
if (element == null)
throw new Error();
return new Group(element);
}
getDirectGroupByIndexOrNull(index) {
const element = this.element.querySelector(`:scope > Group:nth-of-type(${index + 1})`);
if (element == null)
return;
return new Group(element);
}
getDirectGroupByUuidOrThrow(uuid) {
const elements = this.element.querySelectorAll(`:scope > Group`);
for (const element of elements) {
const group = new Group(element);
if (group.getUuidOrThrow().getOrThrow() === uuid)
return group;
continue;
}
throw new Error();
}
getDirectGroupByUuidOrNull(uuid) {
const elements = this.element.querySelectorAll(`:scope > Group`);
for (const element of elements) {
const group = new Group(element);
if (group.getUuidOrThrow().getOrThrow() === uuid)
return group;
continue;
}
return;
}
*getDirectEntries() {
const elements = this.element.querySelectorAll(`:scope > Entry`);
for (const element of elements)
yield new Entry(element);
return;
}
getDirectEntryByIndexOrThrow(index) {
const element = this.element.querySelector(`:scope > Entry:nth-of-type(${index + 1})`);
if (element == null)
throw new Error();
return new Entry(element);
}
getDirectEntryByIndexOrNull(index) {
const element = this.element.querySelector(`:scope > Entry:nth-of-type(${index + 1})`);
if (element == null)
return;
return new Entry(element);
}
getDirectEntryByUuidOrThrow(uuid) {
const elements = this.element.querySelectorAll(`:scope > Entry`);
for (const element of elements) {
const entry = new Entry(element);
if (entry.getUuidOrThrow().getOrThrow() === uuid)
return entry;
continue;
}
throw new Error();
}
getDirectEntryByUuidOrNull(uuid) {
const elements = this.element.querySelectorAll(`:scope > Entry`);
for (const element of elements) {
const entry = new Entry(element);
if (entry.getUuidOrThrow().getOrThrow() === uuid)
return entry;
continue;
}
return;
}
}
KeePassFile.Group = Group;
class Times {
element;
constructor(element) {
this.element = element;
}
getCreationTimeOrThrow() {
const element = this.element.querySelector(":scope > CreationTime");
if (element == null)
throw new Error();
return new Other.AsDate(element);
}
getLastModificationTimeOrNull() {
const element = this.element.querySelector(":scope > LastModificationTime");
if (element == null)
return;
return new Other.AsDate(element);
}
setLastModificationTime(date = new Date()) {
const stale = this.element.querySelector(":scope > LastModificationTime");
if (stale != null) {
const value = new Other.AsDate(stale);
value.setOrThrow(date);
return;
}
const fresh = this.element.ownerDocument.createElement("LastModificationTime");
this.element.appendChild(fresh);
const value = new Other.AsDate(fresh);
value.setOrThrow(date);
}
getLastAccessTimeOrNull() {
const element = this.element.querySelector(":scope > LastAccessTime");
if (element == null)
return;
return new Other.AsDate(element);
}
setLastAccessTime(date = new Date()) {
const stale = this.element.querySelector(":scope > LastAccessTime");
if (stale != null) {
const value = new Other.AsDate(stale);
value.setOrThrow(date);
return;
}
const fresh = this.element.ownerDocument.createElement("LastAccessTime");
this.element.appendChild(fresh);
const value = new Other.AsDate(fresh);
value.setOrThrow(date);
}
getExpiresOrNull() {
const element = this.element.querySelector(":scope > Expires");
if (element == null)
return;
return new Other.AsBoolean(element);
}
getUsageCountOrNull() {
const element = this.element.querySelector(":scope > UsageCount");
if (element == null)
return;
return new Other.AsInteger(element);
}
incrementUsageCount() {
const stale = this.element.querySelector(":scope > UsageCount");
if (stale != null) {
const value = new Other.AsInteger(stale);
value.incrementOrThrow();
return;
}
const fresh = this.element.ownerDocument.createElement("UsageCount");
this.element.appendChild(fresh);
const value = new Other.AsInteger(fresh);
value.setOrThrow(1);
}
getLocationChangedOrNull() {
const element = this.element.querySelector(":scope > LocationChanged");
if (element == null)
return;
return new Other.AsDate(element);
}
setLocationChanged(date = new Date()) {
const stale = this.element.querySelector(":scope > LocationChanged");
if (stale != null) {
const value = new Other.AsDate(stale);
value.setOrThrow(date);
return;
}
const fresh = this.element.ownerDocument.createElement("LocationChanged");
this.element.appendChild(fresh);
const value = new Other.AsDate(fresh);
value.setOrThrow(date);
}
}
KeePassFile.Times = Times;
class Entry {
element;
constructor(element) {
this.element = element;
}
saveOrThrow() {
return this.getHistoryOrNew().pushOrThrow(this);
}
moveOrThrow($group) {
if (this.element.parentNode === $group.element)
return;
this.element.parentNode?.removeChild(this.element);
$group.element.appendChild(this.element);
this.getTimesOrNew().setLocationChanged();
}
trashOrThrow() {
const $file = new KeePassFile(this.element.ownerDocument);
const $meta = $file.getMetaOrThrow();
const $root = $file.getRootOrThrow();
const recybleBinEnabled = $meta.getRecycleBinEnabledOrNull()?.get();
if (!recybleBinEnabled) {
this.element.parentNode?.removeChild(this.element);
return;
}
const recycleBin = $meta.getRecycleBinUuidOrThrow().getOrThrow();
const $recycleBin = $root.getGroupByUuidOrThrow(recycleBin);
this.moveOrThrow($recycleBin);
}
addStringOrThrow(key, value, protect = false) {
const $string = new KeePassFile.String(this.element.ownerDocument.createElement("String"));
{
const $key = new KeePassFile.Other.AsString(this.element.ownerDocument.createElement("Key"));
const $value = new KeePassFile.Value(this.element.ownerDocument.createElement("Value"));
$key.set(key);
$value.set(value);
$value.protected = protect;
$string.element.appendChild($key.element);
$string.element.appendChild($value.element);
}
this.element.appendChild($string.element);
return $string;
}
getUuidOrThrow() {
const element = this.element.querySelector(":scope > UUID");
if (element == null)
throw new Error();
return new Other.AsUuid(element);
}
getTimesOrNew() {
const stale = this.element.querySelector(":scope > Times");
if (stale != null)
return new Times(stale);
const $times = new Times(this.element.ownerDocument.createElement("Times"));
{
const $creationTime = new Other.AsDate(this.element.ownerDocument.createElement("CreationTime"));
$creationTime.setOrThrow(new Date());
$times.element.appendChild($creationTime.element);
}
this.element.appendChild($times.element);
return $times;
}
getHistoryOrNull() {
const element = this.element.querySelector(":scope > History");
if (element == null)
return;
return new History(element);
}
getHistoryOrNew() {
const stale = this.element.querySelector(":scope > History");
if (stale != null)
return new History(stale);
const fresh = this.element.ownerDocument.createElement("History");
this.element.appendChild(fresh);
return new History(fresh);
}
*getStrings() {
const elements = this.element.querySelectorAll(`:scope > String`);
for (const element of elements)
yield new String(element);
return;
}
getStringByIndexOrNull(index) {
const element = this.element.querySelector(`:scope > String:nth-of-type(${index + 1})`);
if (element == null)
return;
return new String(element);
}
getStringByKeyOrNull(key) {
const elements = this.element.querySelectorAll(`:scope > String`);
for (const element of elements) {
const string = new String(element);
if (string.getKeyOrThrow().get() === key)
return string;
continue;
}
return;
}
}
KeePassFile.Entry = Entry;
class String {
element;
constructor(element) {
this.element = element;
}
getKeyOrThrow() {
const element = this.element.querySelector(":scope > Key");
if (element == null)
throw new Error();
return new Other.AsString(element);
}
getValueOrThrow() {
const element = this.element.querySelector(":scope > Value");
if (element == null)
throw new Error();
return new Other.AsString(element);
}
}
KeePassFile.String = String;
class Value {
element;
constructor(element) {
this.element = element;
}
get() {
return this.element.textContent;
}
set(value) {
this.element.textContent = value;
}
get protected() {
return this.element.getAttribute("Protected") === "True";
}
set protected(value) {
if (value)
this.element.setAttribute("Protected", "True");
else
this.element.removeAttribute("Protected");
}
}
KeePassFile.Value = Value;
class History {
element;
constructor(element) {
this.element = element;
}
pushOrThrow($entry) {
const clone = new Entry($entry.element.cloneNode(true));
const history = clone.getHistoryOrNull();
if (history != null)
clone.element.removeChild(history.element);
this.element.prepend(clone.element);
this.cleanOrThrow();
return clone;
}
cleanOrThrow() {
const $file = new KeePassFile(this.element.ownerDocument);
const $meta = $file.getMetaOrThrow();
const historyMaxItems = $meta.getHistoryMaxItemsOrNull()?.getOrThrow();
if (historyMaxItems != null && this.element.children.length > historyMaxItems) {
while (this.element.children.length > historyMaxItems) {
const last = this.element.lastElementChild;
if (last == null)
throw new Error();
this.element.removeChild(last);
}
}
const historyMaxSize = $meta.getHistoryMaxSizeOrNull()?.getOrThrow();
if (historyMaxSize != null) {
for (let bytes = new TextEncoder().encode(new XMLSerializer().serializeToString(this.element)); bytes.length > historyMaxSize; bytes = new TextEncoder().encode(new XMLSerializer().serializeToString(this.element))) {
const last = this.element.lastElementChild;
if (last == null)
throw new Error();
this.element.removeChild(last);
}
}
}
*getDirectEntries() {
const elements = this.element.querySelectorAll(`:scope > Entry`);
for (const element of elements)
yield new Entry(element);
return;
}
getDirectEntryByIndexOrNull(index) {
const element = this.element.querySelector(`:scope > Entry:nth-of-type(${index + 1})`);
if (element == null)
return;
return new Entry(element);
}
}
KeePassFile.History = History;
let Other;
(function (Other) {
class AsString {
element;
constructor(element) {
this.element = element;
}
get() {
return this.element.textContent;
}
set(value) {
this.element.textContent = value;
}
}
Other.AsString = AsString;
class AsBoolean {
element;
constructor(element) {
this.element = element;
}
get() {
return this.element.textContent === "True";
}
set(value) {
this.element.textContent = value ? "True" : "False";
}
}
Other.AsBoolean = AsBoolean;
class AsInteger {
element;
constructor(element) {
this.element = element;
}
getOrThrow() {
const value = this.element.textContent;
if (!value)
throw new Error();
const number = Number(value);
if (!Number.isSafeInteger(number))
throw new Error();
return number;
}
setOrThrow(value) {
if (!Number.isSafeInteger(value))
throw new Error();
this.element.textContent = globalThis.String(value);
}
incrementOrThrow() {
this.setOrThrow(this.getOrThrow() + 1);
}
}
Other.AsInteger = AsInteger;
class AsDate {
element;
constructor(element) {
this.element = element;
}
getOrThrow() {
const value = this.element.textContent;
if (!value)
throw new Error();
const binary = Uint8Array.fromBase64(value);
const cursor = new Cursor(binary);
const raw = cursor.readBigUint64OrThrow(true);
const fix = raw - 62135596800n;
return new Date(Number(fix * 1000n));
}
setOrThrow(value) {
const fix = BigInt(value.getTime()) / 1000n;
const raw = fix + 62135596800n;
const cursor = new Cursor(new Uint8Array(8));
cursor.writeBigUint64OrThrow(raw, true);
this.element.textContent = cursor.bytes.toBase64();
}
}
Other.AsDate = AsDate;
class AsUuid {
element;
constructor(element) {
this.element = element;
}
getOrThrow() {
const base64 = this.element.textContent;
if (!base64)
throw new Error();
const bytes = Uint8Array.fromBase64(base64);
return StringAsUuid.from(bytes);
}
setOrThrow(value) {
const bytes = BytesAsUuid.from(value);
const base64 = bytes.toBase64();
this.element.textContent = base64;
}
}
Other.AsUuid = AsUuid;
})(Other = KeePassFile.Other || (KeePassFile.Other = {}));
})(KeePassFile || (KeePassFile = {}));