diff --git a/.gitignore b/.gitignore index 66fa52b..c7c1ff6 100644 --- a/.gitignore +++ b/.gitignore @@ -176,3 +176,4 @@ dist # confidant env.ts +test diff --git a/confidant.ts b/confidant.ts index ad6945d..e338811 100755 --- a/confidant.ts +++ b/confidant.ts @@ -1,12 +1,17 @@ #!/bin/env bun -import { input, password, select } from "@inquirer/prompts"; +import { checkbox, input, password, select } from "@inquirer/prompts"; import { $ } from "bun"; import chalk from "chalk-template"; import { Command } from "commander"; import { decrypt_diary, encrypt_diary, initialize, recovery } from "./src/main"; import { existsSync } from "fs"; -import checkForFiles, { log, panic } from "./src/utils"; +import checkForFiles, { + getDirectoryNames, + getVaultName, + log, + panic, +} from "./src/utils"; const program = new Command(); const { exit } = process; @@ -18,33 +23,8 @@ program .command("init") .description("initialize a confidant vault") .action(async () => { - // check if a vault already exists - if (await $`test -e config.toml`) { - console.log( - chalk`{red A Confidant vault already exists in this directory.}`, - ); - exit(1); - } + const dirname = (await getDirectoryNames()) as string; - // select a directory - let dirs; - try { - dirs = (await $`ls -d */`.text()).trim(); - } catch (e) { - console.error( - 'No directories found in current directory, please create one and run "init" again.', - ); - process.exit(1); - } - - let dirlist = dirs.split("\n"); - const dirname = await select({ - message: "Select a directory to use:", - choices: dirlist.map((x) => ({ - name: x, - value: x, - })), - }); const pass = await password({ message: chalk`{reset {yellow Enter a password to use:}}`, mask: "•", @@ -54,8 +34,7 @@ program mask: "•", }); if (pass !== confpass) { - console.log(chalk`Passwords don't math.`); - exit(1); + panic`Passwords don't match. Exiting...`; } await initialize(pass, dirname); @@ -66,14 +45,18 @@ program .command("decrypt") .description("decrypt the vault") .option("-l, --live", "decrypt in live mode") + .option("-v, --vault ", "name of the vault to decrypt") .action(async (args) => { - checkForFiles(); + let files: string[]; + if (!args.vault) { + args.vault = await getVaultName(); + } const pass = await password({ message: chalk`{reset {yellow Enter the password:}}`, mask: "•", }); - await decrypt_diary(pass); + await decrypt_diary(pass, args.vault); if (args.live) { await input({ message: chalk`{yellow Live mode started. Press ENTER to encrypt}`, diff --git a/package.json b/package.json index e25e1a3..c7a877b 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "init": "bun ./scripts/init.ts", + "sh": "bun ./scripts/sh.ts", "build": "bun ./scripts/build.ts" }, "bin": { diff --git a/src/main.ts b/src/main.ts index 02883d2..03a961e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,7 +14,9 @@ import { hmac, log, panic, + print, random, + separator, stringy, } from "./utils"; import env from "../env"; @@ -27,136 +29,104 @@ console.info = function () {}; const { exit } = process; export async function initialize(password: string, dirname: string) { - // compressing and encrypting diary - const [K_C, P_C] = ECDH(); - const [K_F, P_F] = ECDH(); - const S_CF = await derive(K_C, P_F); - const fidsalt = randomBytes(32); - const fidcode = random(10000, 100000); - const D = pbkdf2(S_CF, fidsalt, fidcode, 64, "sha256"); + // compressing and encrypting vault + const [K_A, P_A] = ECDH(); + const [K_B, P_B] = ECDH(); + const S_AB = await derive(K_A, P_B); + const salt = randomBytes(32); + const code = random(10000, 100000); + const D = pbkdf2(S_AB, salt, code, 64, "sha256"); await $`zip -r9 confidant.zip ${dirname} > /dev/null`; await $`rm -rf ${dirname}`; const Z = readFileSync("confidant.zip"); const E_Z = encrypt_file(Z, D); - writeFileSync(`vault.ant`, E_Z); - // creating key.fid - const fidData = { - privateKey: stringy(K_F), - salt: stringy(fidsalt), - code: fidcode, + // creating dirname.key + const keyfile = { + salt: stringy(salt), + code: code, + privateKey: stringy(K_A), }; - const fidkey = randomBytes(32); - const consalt = randomBytes(32); - const D_C = hmac(consalt, fidkey); - const E_F = encrypt_file(buffer(stringify(fidData), "utf8"), D_C); - writeFileSync(`key.fid`, E_F); - - // create data.con const auth_secret = randomBytes(32); - const conData = { - privateKey: stringy(K_C), - fidkey: stringy(fidkey), - consalt: stringy(consalt), - }; - const D_U = hmac(auth_secret, buffer(env.AUTH_KEY)); - const E_C = encrypt_file(buffer(stringify(conData), "utf8"), D_U); - writeFileSync("data.con", E_C); + const confsalt = randomBytes(64); + const K = hmac(auth_secret, confsalt); + const E_K = encrypt_file(Buffer.from(JSON.stringify(keyfile), "utf8"), K); + writeFileSync(`${dirname}.key`, E_K); + // creating config data const recovery_phrase = generate_recovery_phrase(); - writeFileSync("recovery.txt", recovery_phrase + "\n"); + writeFileSync(`${dirname}_recovery.txt`, recovery_phrase + "\n"); const recovery_auth_key = HmacSHA256(recovery_phrase, env.AUTH_KEY).toString( enc.Base64, ); const password_auth_key = HmacSHA256(password, env.AUTH_KEY).toString( enc.Base64, ); - - // create config.toml - const configData = { - config: { - con: "data.con", - fid: "key.fid", - ant: "vault.ant", - dir: dirname, - keystore: stringy(encrypt(auth_secret, buffer(password_auth_key))), - recoverystore: stringy(encrypt(auth_secret, buffer(recovery_auth_key))), - phrasestore: AES.encrypt(env.PHRASE, password_auth_key).toString(), - recphrasestore: AES.encrypt(env.PHRASE, recovery_auth_key).toString(), - }, + const D_C = { + dir: dirname, + confsalt: stringy(confsalt), + privateKey: stringy(K_B), + keystore: stringy(encrypt(auth_secret, buffer(password_auth_key))), + recoverystore: stringy(encrypt(auth_secret, buffer(recovery_auth_key))), + phrasestore: AES.encrypt(env.PHRASE, password_auth_key).toString(), + recphrasestore: AES.encrypt(env.PHRASE, recovery_auth_key).toString(), }; - writeFileSync("config.toml", buffer(stringify(configData), "utf8")); + + // create dirname.vault + writeFileSync( + `${dirname}.vault`, + Buffer.concat([Buffer.from(JSON.stringify(D_C)), separator, E_Z]), + ); + console.log(`Recovery phrase:`); console.log(chalk`{magenta ${recovery_phrase}}`); await $`rm confidant.zip`; + // create .gitignore const gitignore = `# .gitignore -data.con -key.fid -recovery.txt +*.con +*.fid +*_recovery.txt confidant.zip -.confidant -${dirname} -.env +*.confidant `; - writeFileSync(".gitignore", gitignore); + writeFileSync(`.gitignore`, gitignore); } -export async function decrypt_diary(password: string) { +export async function decrypt_diary(password: string, dirname: string) { + // Read and split the vault file + const combined = readFileSync(`${dirname}.vault`); + const index = combined.indexOf(separator); + const D_C = combined.subarray(0, index).toString("utf8"); + const E_Z = combined.subarray(index + separator.length); + // Check if password is correct - const config = Object( - parse(readFileSync("config.toml").toString("utf8")), - ) as Config; const password_auth_key = HmacSHA256(password, env.AUTH_KEY).toString( enc.Base64, ); - const dec = AES.decrypt( - config.config.phrasestore, - password_auth_key, - ).toString(enc.Utf8); - if (dec !== env.PHRASE) { - panic`Wrong password. Try again or reset it.`; + const config = JSON.parse(D_C); + const phrase = AES.decrypt(config.phrasestore, password_auth_key).toString( + enc.Utf8, + ); + if (phrase !== env.PHRASE) { + panic`Incorrect password. Try again or reset it.`; } - // Decrypt data.con - const auth_secret = decrypt( - buffer(config.config.keystore), - buffer(password_auth_key), - ); - const D_U = hmac(auth_secret, buffer(env.AUTH_KEY)); - const conData = Object( - parse(decrypt_file(readFileSync(config.config.con), D_U).toString("utf8")), - ) as ConData; + const auth_secret = decrypt(config.keystore, buffer(password_auth_key)); + const K = hmac(auth_secret, config.confsalt); + const E_K = readFileSync(`${dirname}.key`); + const keyfile = JSON.parse(decrypt_file(E_K, K).toString("utf8")); - // Decrypt key.fid - const { consalt, fidkey, privateKey: conK } = conData; - const D_C = hmac(buffer(consalt), buffer(fidkey)); - const fidData = Object( - parse(decrypt_file(readFileSync(config.config.fid), D_C).toString("utf8")), - ) as FidData; - const { privateKey: fidK, salt, code } = fidData; + const K_A = buffer(keyfile.privateKey); + const K_B = buffer(config.privateKey); + const P_B = getPublic(K_B); + const S_AB = await derive(K_A, P_B); + const D = pbkdf2(S_AB, buffer(keyfile.salt), keyfile.code, 64, "sha256"); - // Decrypt diary.ant - const S_CF = await derive(buffer(conK), getPublic(buffer(fidK))); - const D = pbkdf2(S_CF, buffer(salt), code, 64, "sha256"); - writeFileSync( - ".confidant", - encrypt( - Buffer.from( - JSON.stringify({ - dirname: config.config.dir, - key: stringy(D), - }), - "utf8", - ), - buffer(env.AUTH_KEY), - ), - ); - writeFileSync( - "confidant.zip", - decrypt_file(readFileSync(config.config.ant), D), - ); + const Z = decrypt_file(E_Z, D); + writeFileSync(`confidant.zip`, Z); + writeFileSync(`.${dirname}.confidant`, encrypt(D, buffer(env.AUTH_KEY))); await $`unzip confidant.zip > /dev/null && rm confidant.zip`; } diff --git a/src/utils.ts b/src/utils.ts index 8845a45..18d3698 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,7 @@ import { generate } from "random-words"; import chalk from "chalk-template"; import { $ } from "bun"; import { existsSync } from "fs"; +import { select } from "@inquirer/prompts"; export function buffer(input: string): Buffer; export function buffer(input: string, encoding: BufferEncoding): Buffer; @@ -69,8 +70,11 @@ export async function exists(dir: string) { } } -export function log(str: TemplateStringsArray, ...placeholders: unknown[]) { - console.log(chalk(str, placeholders)); +export function log( + str: TemplateStringsArray, + ...placeholders: unknown[] +): void { + console.log(chalk(str as TemplateStringsArray, ...placeholders)); } export function panic(str: TemplateStringsArray) { @@ -92,3 +96,78 @@ export default function checkForFiles() { process.exit(1); } } + +export const separator = Buffer.from([ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, +]); + +export function print(key: string, value: string) { + console.log(chalk`{green ${key}:} {blue ${value}}`); +} + +export async function getDirectoryNames() { + const dirlist = (await $`ls -d */`.text()).trim(); + + if (!dirlist) { + panic`No directories found in current directory, please create one and run "init" again.`; + return; + } + let dirs: string[]; + const vaultList = (await $`ls *.vault`.text()).trim().match(/.*\.vault/g); + if (vaultList) { + const vaults = vaultList.map((x) => x.replace(".vault", "")); + dirs = Array.from(new Set(dirlist.split("\n")).difference(new Set(vaults))); + } else { + dirs = dirlist.split("\n"); + } + + if (dirs.length === 0) { + panic`No usable directories found in current directory, please create one and run "init" again.`; + return; + } else if (dirs.length === 1) { + return dirs[0]; + } + + const dirname = await select({ + message: "Select a directory to use:", + choices: dirs.map((x) => ({ + name: x, + value: x, + })), + }); + + return dirname; +} + +export async function getVaultName() { + const vaultList = (await $`ls *.vault`.text()).trim().match(/.*\.vault/g); + if (!vaultList) { + panic`No vaults found in the current directory.`; + return; + } + + let files: string[]; + const decryptedList = (await $`bash -c "ls .*.confidant"`.text()) + .trim() + .match(/\.(.*)\.confidant/g); + if (decryptedList) { + const vaults = vaultList.map((x) => x.replace(".vault", "")); + const decs = decryptedList.map((x) => x.replace(/\.(.*)\.confidant/, "$1")); + files = Array.from(new Set(vaults).difference(new Set(decs))); + } else { + files = vaultList.map((x) => x.replace(".vault", "")); + } + if (files.length === 1) { + return files[0]; + } + + const vault = await select({ + message: "Select vault to decrypt:", + choices: files.map((x) => ({ + name: x.replace(".vault", ""), + value: x.replace(".vault", ""), + })), + }); + return vault; +}