diff --git a/repl.ts b/repl.ts index ef29dfd..ac1c8bd 100755 --- a/repl.ts +++ b/repl.ts @@ -1,4 +1,5 @@ #!/usr/bin/env ts-node-transpile-only +import fs from "fs" import chalk from "chalk" import prompts from "prompts" import { OnePassword } from "./src/index" @@ -7,24 +8,35 @@ async function main(args: string[]) { const instance = new OnePassword({ path: args[0] }) const profiles = await instance.getProfileNames() - const { profile } = await prompts({ - type: "select", - name: "profile", - choices: profiles.map(t => ({ title: t, value: t })), - message: "Which vault?", - }) + // const { profile } = await prompts({ + // type: "select", + // name: "profile", + // choices: profiles.map(t => ({ title: t, value: t })), + // message: "Which vault?", + // }) - console.log(chalk`You have chosen {green ${profile}}.`) + // console.log(chalk`You have chosen {green ${profile}}.`) + const profile = "default" const vault = await instance.getProfile(profile) - const { password } = await prompts({ - type: "invisible", - name: "password", - message: "Master Password?", - }) + // const { password } = await prompts({ + // type: "invisible", + // name: "password", + // message: "Master Password?", + // }) + const password = "freddy" vault.unlock(password) - console.log(vault.overviews.values()) + + const d = vault.decryptAttachment( + fs.readFileSync( + "./freddy-2013-12-04.opvault/default/1C7D72EFA19A4EE98DB7A9661D2F5732_3B94A1F475014E27BFB00C99A42214DF.attachment" + ) + ) + + fs.writeFileSync("./test", d) + + // console.log(vault.overviews.values()) } main(process.argv.slice(2)) diff --git a/src/crypto.ts b/src/crypto.ts new file mode 100644 index 0000000..cdeedb9 --- /dev/null +++ b/src/crypto.ts @@ -0,0 +1,27 @@ +import { webcrypto } from "crypto" + +declare module "crypto" { + export const webcrypto: Crypto +} + +async function pbkdf2(password: string, salt: string, iterations = 1000, length = 256) { + const encoder = new TextEncoder() + const key = await webcrypto.subtle.importKey( + "raw", + encoder.encode(password), + "PBKDF2", + false, + ["deriveBits"] + ) + const bits = await webcrypto.subtle.deriveBits( + { + name: "PBKDF2", + hash: "SHA-512", + salt: encoder.encode(salt), + iterations, + }, + key, + length + ) + return bits +} diff --git a/src/types.ts b/src/types.ts index 172c0f9..d1ee3b6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,32 +1,46 @@ import type { FieldType } from "./models" +type integer = number + export interface Profile { lastUpdatedBy: "Dropbox" /** Unix seconds */ - updatedAt: number + updatedAt: integer profileName: string salt: string // base64 masterKey: string // base64 - iterations: number // 50000 + iterations: integer // 50000 uuid: string // 32 chars overviewKey: string // "b3B...IMO52D" - createdAt: number // Unix seconds + createdAt: integer // Unix seconds } export interface EncryptedItem { category: string // "001" /** Unix seconds */ - created: number + created: integer d: string // "b3BkYXRhMbt" folder: string // 32 chars hmac: string // base64 k: string // base64 o: string // base64 - tx: number // Unix seconds - updated: number // Unix seconds + tx: integer // Unix seconds + updated: integer // Unix seconds uuid: string // 32 chars } +export interface AttachmentMetadata { + itemUUID: string + contentSize: integer + external: boolean + updatedAt: integer + txTimestamp: integer + /** Base64 encoded OPData */ + overview: string + createdAt: integer + uuid: string +} + export type TextField = { type: FieldType.Text value: string diff --git a/src/vault.ts b/src/vault.ts index f1358a1..97d976f 100644 --- a/src/vault.ts +++ b/src/vault.ts @@ -4,7 +4,14 @@ import type { IFileSystem } from "./fs" import { OnePasswordFileManager } from "./fs" import { i18n } from "./i18n" -import type { Profile, Band, Overview, EncryptedItem, Item } from "./types" +import type { + Profile, + Band, + Overview, + EncryptedItem, + Item, + AttachmentMetadata, +} from "./types" type FoldersMap = { [uuid: string]: Band } @@ -89,11 +96,11 @@ export class Vault { unlock(masterPassword: string) { const profile = this.#profile const derivedKey = crypto.pbkdf2Sync( - masterPassword, - toBuffer(profile.salt), - profile.iterations, + /* password */ masterPassword, + /* salt */ toBuffer(profile.salt), + /* iterations */ profile.iterations, /* keylen */ 64, - "sha512" + /* digest */ "sha512" ) const cipher = splitPlainText(derivedKey) @@ -113,6 +120,10 @@ export class Vault { return this } + decryptAttachment(buffer: Buffer) { + return decryptAttachment(buffer, this.#master!) + } + /** * Remove derived keys stored within the class instance. */ @@ -175,7 +186,10 @@ function decryptItem(item: EncryptedItem, master: Cipher): Item { } function decryptAttachment(item: Buffer, master: Cipher) { - invariant(item.slice(0, 7).toString("utf-8") === "OPCLDA") + invariant( + item.slice(0, 6).toString("utf-8") === "OPCLDA", + "Attachment must start with OPCLDA" + ) invariant( item.readIntLE(7, 1) === 1, "The version for this attachment file format is not supported." @@ -184,8 +198,13 @@ function decryptAttachment(item: Buffer, master: Cipher) { 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 metadata: AttachmentMetadata = JSON.parse( + item.slice(16, 16 + metadataSize).toString("utf-8") + ) const icondata = item.slice(16 + metadataSize, 16 + metadataSize + iconSize) + console.log(icondata.slice(0, 8).toString()) + const iconData = decryptOPData(icondata, master) + return iconData } /** Encryption and MAC */