diff --git a/README.md b/README.md index c94389e..9d85a9f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Testing ```sh -wget -qO- https://cache.agilebits.com/security-kb/freddy-2013-12-04.tar.gz | tar xvz - +wget -qO- https://cache.agilebits.com/security-kb/freddy-2013-12-04.tar.gz | tar xvz mv onepassword_data freddy-2013-12-04.opvault pnpm run test ``` diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..21f0919 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,11 @@ +export abstract class OPVaultError extends Error {} + +export class AssertionError extends OPVaultError {} + +export class HMACAssertionError extends AssertionError {} + +export function invariant(condition: any, message?: string): asserts condition { + if (!condition) { + throw new AssertionError(message) + } +} diff --git a/src/models.ts b/src/models.ts index b9c89b6..3f32d95 100644 --- a/src/models.ts +++ b/src/models.ts @@ -1,4 +1,4 @@ -import invariant from "tiny-invariant" +import { invariant } from "./errors" export enum Category { Login = 1, diff --git a/src/vault.ts b/src/vault.ts index accd3ae..f1358a1 100644 --- a/src/vault.ts +++ b/src/vault.ts @@ -1,4 +1,5 @@ import * as crypto from "crypto" +import { HMACAssertionError, invariant } from "./errors" import type { IFileSystem } from "./fs" import { OnePasswordFileManager } from "./fs" import { i18n } from "./i18n" @@ -44,7 +45,9 @@ export class Vault { */ static async of(path: string, profileName = "default", fs: IFileSystem) { const files = new OnePasswordFileManager(fs, path, profileName) - const profile = JSON.parse(stripText(await files.getProfile(), "var profile=", ";")) + const profile = JSON.parse( + stripText(await files.getProfile(), /^var profile\s*=/, ";") + ) const folders = JSON.parse(stripText(await files.getFolders(), "loadFolders(", ");")) const bands = new Map() @@ -111,7 +114,7 @@ export class Vault { } /** - * Remove derived keys within the class instance. + * Remove derived keys stored within the class instance. */ lock() { this.#master = null! @@ -171,6 +174,20 @@ function decryptItem(item: EncryptedItem, master: Cipher): Item { return JSON.parse(detail.toString("utf-8")) } +function decryptAttachment(item: Buffer, master: Cipher) { + invariant(item.slice(0, 7).toString("utf-8") === "OPCLDA") + invariant( + item.readIntLE(7, 1) === 1, + "The version for this attachment file format is not supported." + ) + + const metadataSize = item.readIntLE(8, 2) + const iconSize = item.readIntLE(12, 3) + + const metadata = JSON.parse(item.slice(16, 16 + metadataSize).toString("utf-8")) + const icondata = item.slice(16 + metadataSize, 16 + metadataSize + iconSize) +} + /** Encryption and MAC */ interface Cipher { /** Encryption key */ @@ -179,9 +196,27 @@ interface Cipher { hmac: Buffer } -function stripText(text: string, prefix: string, suffix: string) { - if (text.startsWith(prefix)) text = text.slice(prefix.length) - if (text.endsWith(suffix)) text = text.slice(0, -suffix.length) +function stripText(text: string, prefix: string | RegExp, suffix: string | RegExp) { + if (typeof prefix === "string") { + if (text.startsWith(prefix)) { + text = text.slice(prefix.length) + } + } else { + const prefixMatch = text.match(prefix) + if (prefixMatch) { + text = text.slice(prefixMatch[0].length) + } + } + if (typeof suffix === "string") { + if (text.endsWith(suffix)) { + text = text.slice(0, -suffix.length) + } + } else { + const suffixMatch = text.match(suffix) + if (suffixMatch) { + text = text.slice(0, -suffixMatch[0].length) + } + } return text } @@ -190,8 +225,6 @@ const splitPlainText = (derivedKey: Buffer): Cipher => ({ hmac: derivedKey.slice(32, 64), }) -class HMACAssertionError extends Error {} - function decryptOPData(cipherText: Buffer, cipher: Cipher) { const key = cipherText.slice(0, -32) assertHMac(key, cipher.hmac, cipherText.slice(-32))