UNPKG

@hazae41/kdbx

Version:

Rust-like KeePass (KDBX 4) file format for TypeScript

826 lines (825 loc) 32.8 kB
// 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 = {}));