diff --git a/confidant.ts b/confidant.ts index 476a4d2..09557e4 100755 --- a/confidant.ts +++ b/confidant.ts @@ -5,13 +5,17 @@ import { $ } from "bun"; import chalk from "chalk-template"; import { Command } from "commander"; import { decrypt_vault, encrypt_vault, initialize, recovery } from "./src/main"; -import { existsSync } from "fs"; +import { existsSync, readdirSync } from "fs"; import checkForFiles, { + Files, getDecryptedName, getDirectoryNames, + getRandomPassword, getVaultName, + log, panic, } from "./src/utils"; +import { randomBytes } from "crypto"; const program = new Command(); const { exit } = process; @@ -22,16 +26,29 @@ program.name("confidant").description("Creates a very secure file vault."); program .command("init") .description("initialize a confidant vault") - .action(async () => { - const dirname = (await getDirectoryNames()) as string; + .argument("[directory]", "Directory to use to create a vault") + .action(async (dirname) => { + if (!dirname) { + dirname = (await getDirectoryNames()) as string; + } + const selectedDir = readdirSync(".").filter((x) => x.match(dirname))[0]; - const pass = await password({ + if (!selectedDir) { + console.log( + chalk`{red Directory "${dirname}" not found in current location.}`, + ); + exit(1); + } + + log`{blue Using "{green ${dirname}}" to create a vault...}`; + const genPass = getRandomPassword(14); + const pass = await input({ message: chalk`{reset {yellow Enter a password to use:}}`, - mask: "•", + default: genPass, }); - const confpass = await password({ + const confpass = await input({ message: chalk`{reset {yellow Enter the password again:}}`, - mask: "•", + default: genPass, }); if (pass !== confpass) { panic`Passwords don't match. Exiting...`; @@ -44,24 +61,35 @@ program program .command("decrypt") .description("decrypt the vault") + .argument("[vault]", "Name of the vault to decrypt") .option("-l, --live", "decrypt in live mode") - .option("-v, --vault ", "name of the vault to decrypt") - .action(async (args) => { - if (!args.vault) { - args.vault = await getVaultName(); + .action(async (args, opts) => { + if (!args) { + args = await getVaultName(); + } else { + const vaults = new Files(/(.*).vault/g).intersection( + new Files(/(.*).key/g), + ).data as string[]; + if (!vaults.includes(args)) { + console.log( + chalk`{red Vault "${args}" not found in current directory.}`, + ); + exit(1); + } } + log`{blue Decrypting "{green ${args}}"...}`; const pass = await password({ message: chalk`{reset {yellow Enter the password:}}`, mask: "•", }); - await decrypt_vault(pass, args.vault); - if (args.live) { + await decrypt_vault(pass, args); + if (opts.live) { await input({ - message: chalk`{yellow Live mode started. Press ENTER to encrypt}`, + message: chalk`{yellow Live mode started. Press ENTER to encrypt:}`, }); - await encrypt_vault(args.vault); - console.log(chalk`{green Successfully encrypted!}`); + await encrypt_vault(args); + console.log(chalk`{green Encrypted successfully!}`); } else { console.log(chalk`{green Decrypted sucessfully!}`); } @@ -70,13 +98,23 @@ program program .command("encrypt") .description("encrypt the vault") - .option("-v, --vault ", "name of the vault to decrypt") - .action(async (args) => { - if (!args.vault) { - args.vault = await getDecryptedName(); + .argument("[vault]", "Name of the vault to decrypt") + .action(async (vault) => { + if (!vault) { + vault = await getDecryptedName(); + } else { + const vaults = new Files(/(.*).vault/g).intersection( + new Files(/\.(.*)\.confidant/g), + ).data as string[]; + if (!vaults.includes(vault)) { + console.log( + chalk`{red Vault "${vault}" not found in current directory.}`, + ); + exit(1); + } } - await encrypt_vault(args.vault); + await encrypt_vault(vault); console.log(chalk`{green Successfully encrypted!}`); }); diff --git a/scripts/build.ts b/scripts/build.ts index c30aa1d..06b20bf 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -7,5 +7,5 @@ await $` rm -rf ./dist bun build --compile --target=bun-linux-x64 ./confidant.ts --outfile ./dist/confidant_${ver}_linux_x64 bun build --compile --target=bun-windows-x64 ./confidant.ts --outfile ./dist/confidant_${ver}_win_x64.exe -bun build --compile --target=bun-darwin-x64 ./confidant.ts --outfile ./dist/confidant_${ver}_darwin_x64 +bun build --compile --target=bun-darwin-arm64 ./confidant.ts --outfile ./dist/confidant_${ver}_darwin_arm64 `; diff --git a/src/main.ts b/src/main.ts index a01bbcc..79c5147 100644 --- a/src/main.ts +++ b/src/main.ts @@ -75,7 +75,11 @@ export async function initialize(password: string, dirname: string) { // create dirname.vault writeFileSync( `${dirname}.vault`, - Buffer.concat([Buffer.from(JSON.stringify(D_C)), separator, E_Z]), + Buffer.concat([ + Buffer.from(Buffer.from(JSON.stringify(D_C)).toString("base64")), + separator, + E_Z, + ]), ); console.log(`Recovery phrase:`); @@ -95,62 +99,70 @@ confidant.zip } export async function decrypt_vault(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); + try { + // 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 password_auth_key = HmacSHA256(password, env.AUTH_KEY).toString( - enc.Base64, - ); - 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.`; + // Check if password is correct + const password_auth_key = HmacSHA256(password, env.AUTH_KEY).toString( + enc.Base64, + ); + const config = JSON.parse(Buffer.from(D_C, "base64").toString()); + const phrase = AES.decrypt(config.phrasestore, password_auth_key).toString( + enc.Utf8, + ); + if (phrase !== env.PHRASE) { + panic`Incorrect password. Try again or reset it.`; + } + + 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")); + + 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"); + + 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`; + } catch (e) { + panic`Error decrypting vault. The files could be corrupted.`; } - - 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")); - - 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"); - - 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`; } export async function encrypt_vault(dirname: string) { - const D = decrypt( - readFileSync(`.${dirname}.confidant`), - buffer(env.AUTH_KEY), - ); - // create zip file and encrypt it - await $`zip -r9 confidant.zip ${dirname} > /dev/null`; - await $`rm -rf ${dirname}`; - const Z = readFileSync("confidant.zip"); - const E_Z = encrypt_file(Z, D); + try { + const D = decrypt( + readFileSync(`.${dirname}.confidant`), + buffer(env.AUTH_KEY), + ); + // create zip file and encrypt it + await $`zip -r9 confidant.zip ${dirname} > /dev/null`; + await $`rm -rf ${dirname}`; + const Z = readFileSync("confidant.zip"); + const E_Z = encrypt_file(Z, D); - // read original vault file to get header data - const vaultfile = readFileSync(`${dirname}.vault`); - const index = vaultfile.indexOf(separator); + // read original vault file to get header data + const vaultfile = readFileSync(`${dirname}.vault`); + const index = vaultfile.indexOf(separator); - // write new data to vault - writeFileSync( - `${dirname}.vault`, - Buffer.concat([vaultfile.subarray(0, index), separator, E_Z]), - ); - await $`rm confidant.zip .${dirname}.confidant`; + // write new data to vault + writeFileSync( + `${dirname}.vault`, + Buffer.concat([vaultfile.subarray(0, index), separator, E_Z]), + ); + await $`rm confidant.zip .${dirname}.confidant`; + } catch (e) { + panic`Error encrypting vault. The files could be corrupted.`; + } } export async function recovery(recoverystring: string) { diff --git a/src/utils.ts b/src/utils.ts index 7ab774e..9226a1e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -109,7 +109,10 @@ export function print(key: string, value: string) { export class Files { data: string[] | null = null; - constructor(regex: RegExp | string) { + constructor(regex?: RegExp | string) { + if (!regex) { + return this; + } try { const data = readdirSync(".").filter((x) => x.match(regex)); this.data = data ? data.map((x) => x.replace(regex, "$1")) : null; @@ -119,16 +122,24 @@ export class Files { } } + empty(): boolean { + return this.data === null || this.data.length === 0; + } + toString(): string { return `[ ${this.data ? this.data.join(", ") : "empty"} ]`; } intersection(a: Files) { - return Array.from(new Set(this.data).intersection(new Set(a.data))); + const obj = new Files(); + obj.data = Array.from(new Set(this.data).intersection(new Set(a.data))); + return obj; } difference(a: Files) { - return Array.from(new Set(this.data).difference(new Set(a.data))); + const obj = new Files(); + obj.data = Array.from(new Set(this.data).difference(new Set(a.data))); + return obj; } } @@ -167,35 +178,25 @@ export async function getDirectoryNames() { } export async function getVaultName() { - const vaultList = (await $`ls *.vault`.text()).trim().match(/.*\.vault/g); - if (!vaultList) { + const vaultList = new Files(/(.*)\.vault/g); + if (vaultList.empty()) { panic`No vaults found in the current directory.`; return; } - const keyList = (await $`ls *.key`.text()) - .trim() - .match(/.*\.key/g) as string[]; - if (!keyList) { + const keyList = new Files(/(.*)\.key/g); + if (keyList.empty()) { panic`No vaults with keys found in the current directory.`; return; } - const vaults = Array.from( - new Set(vaultList.map((x) => x.replace(".vault", ""))).intersection( - new Set(keyList.map((x) => x.replace(".key", ""))), - ), - ); - + const vaults = vaultList.intersection(keyList); let files: string[]; - const decryptedList = (await $`bash -c "ls .*.confidant"`.text()) - .trim() - .match(/\.(.*)\.confidant/g); - if (decryptedList) { - const decs = decryptedList.map((x) => x.replace(/\.(.*)\.confidant/, "$1")); - files = Array.from(new Set(vaults).difference(new Set(decs))); + const decryptedList = new Files(/\.(.*)\.confidant/g); + if (decryptedList.empty()) { + files = vaults.difference(decryptedList).data as string[]; } else { - files = vaultList.map((x) => x.replace(".vault", "")); + files = vaults.data as string[]; } if (files.length === 1) { return files[0]; @@ -203,11 +204,21 @@ export async function getVaultName() { const vault = await select({ message: "Select vault to decrypt:", - choices: files.map((x) => ({ - name: x.replace(".vault", ""), - value: x.replace(".vault", ""), - })), + choices: [ + ...files.map((x) => ({ + name: x.replace(".vault", ""), + value: x.replace(".vault", ""), + })), + { + name: chalk`{red EXIT}`, + value: "exit", + }, + ], }); + if (vault === "exit") { + log`{yellow Exiting...}`; + process.exit(0); + } return vault; } @@ -222,7 +233,7 @@ export async function getDecryptedName() { panic`No vaults found.`; } - const decrypted = dec.intersection(vaults); + const decrypted = dec.intersection(vaults).data as string[]; if (decrypted.length === 1) { return decrypted[0]; @@ -238,3 +249,14 @@ export async function getDecryptedName() { return vault; } + +export function getRandomPassword(length: number) { + const STRING = + "0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + let pass = ""; + + while (pass.length < length) { + pass += STRING[Math.floor(Math.random() * STRING.length)]; + } + return pass; +}