commit 2881d9b8608d322c2e44ef6345098b403450964d Author: Suraj B M Date: Sun Sep 29 12:58:05 2024 +0530 init: inital commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66fa52b --- /dev/null +++ b/.gitignore @@ -0,0 +1,178 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +# confidant +env.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..327cb5f --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Confidant + +## Table of Contents +1. [Introduction](#introduction) +2. [Installation](#installation) + a. [Linux](#linux) + b. [Windows](#windows) + c. [MacOS](#macos) +3. [Usage](#usage) +4. [Build from source](#build-from-source) +--- + +## Introduction +Confidant is a CLI tool used to create a triple-layer protected vault, written in TypeScript. It makes use of a combination of ECDH, AES256 and HMAC-SHA256 to create the vault, which can be acessible only if 3 particular files, namely `data.con`, `key.fid` and `vault.ant` (and a `config.toml`), are present. It also requires a password to start the decryption process. In case the password is lost, the vault can be recovered using the recovery phrase, which is a 12-word phrase that is generated during the vault creation process. + +## Installation +### Linux +1. Download the latest release from the releases page. +2. Give it executable permissions by running `chmod +x confidant`. +3. Move it to a directory in your PATH, like `/usr/local/bin`. +4. Run `confidant --help` to verify the installation. + +### Windows +1. Download the latest release from the releases page. +2. Move it to a directory in your PATH. +3. Run `confidant --help` to verify the installation. + +### MacOS +1. Download the latest release from the releases page. +2. Give it executable permissions by running `chmod +x confidant`. +3. Move it to a directory in your PATH, like `/usr/local/bin`. +4. Run `confidant --help` to verify the installation. + +--- + +## Usage +### Create a new vault +To create a new vault, run the following command: +```bash +confidant init +``` +This will show a list of directories in your current directory. Select the directory where you want to create the vault. Also specify a password to encrypt the vault. The recovery phrase will be shown after the vault is created. Save it in a safe place. The following files will be created: +- `data.con`: Primary key +- `key.fid`: Secondary key +- `vault.ant`: Encrypted vault +- `config.toml`: Configuration file +- `.gitignore`: To ignore the vault files +After this, you can push the vault files to a remote repository. The `.gitignore` file will make sure the key files are not pushed to the repository. Make sure to never store the key files in the same place as the vault files. + +### Decrypt a vault +To decrypt a vault, run the following command: +```bash +confidant decrypt +``` +Make sure all the files `data.con`, `key.fid` and `vault.ant` are present in the current directory. Also make sure you have the password and the recovery phrase. The vault will be decrypted and the contents will be shown. + +### Encrypt a vault +To encrypt a vault, run the following command: +```bash +confidant encrypt +``` +Make sure the files `data.con`, `key.fid` and `vault.ant` are present in the current directory. The vault will be encrypted and the files will be updated, after which you can move them to a safe place. diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..4a345c0 Binary files /dev/null and b/bun.lockb differ diff --git a/confidant.ts b/confidant.ts new file mode 100755 index 0000000..ad6945d --- /dev/null +++ b/confidant.ts @@ -0,0 +1,114 @@ +#!/bin/env bun + +import { 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"; + +const program = new Command(); +const { exit } = process; +$.nothrow(); + +program.name("confidant").description("Creates a very secure file vault."); + +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); + } + + // 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: "•", + }); + const confpass = await password({ + message: chalk`{reset {yellow Enter the password again:}}`, + mask: "•", + }); + if (pass !== confpass) { + console.log(chalk`Passwords don't math.`); + exit(1); + } + + await initialize(pass, dirname); + console.log(chalk`{green Initialized a new Confidant vault sucessfully!}`); + }); + +program + .command("decrypt") + .description("decrypt the vault") + .option("-l, --live", "decrypt in live mode") + .action(async (args) => { + checkForFiles(); + + const pass = await password({ + message: chalk`{reset {yellow Enter the password:}}`, + mask: "•", + }); + await decrypt_diary(pass); + if (args.live) { + await input({ + message: chalk`{yellow Live mode started. Press ENTER to encrypt}`, + }); + await encrypt_diary(); + console.log(chalk`{green Successfully encrypted!}`); + } else { + console.log(chalk`{green Decrypted sucessfully!}`); + } + }); + +program + .command("encrypt") + .description("encrypt the vault") + .action(async () => { + checkForFiles(); + + if (!existsSync(".confidant")) { + panic`The vault was not decrypted yet!`; + } + + await encrypt_diary(); + console.log(chalk`{green Successfully encrypted!}`); + }); + +program + .command("recover") + .description("recover vault when password is forgotten") + .action(async () => { + checkForFiles(); + + const recphrase = await input({ + message: chalk`{reset {yellow Enter the recovery phrase:}}`, + }); + recovery(recphrase); + }); + +await program.parseAsync(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..e25e1a3 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "confidant", + "version": "1.0.0", + "module": "confidant.ts", + "type": "module", + "scripts": { + "init": "bun ./scripts/init.ts", + "build": "bun ./scripts/build.ts" + }, + "bin": { + "confidant": "./confidant.ts" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@iarna/toml": "^2.2.5", + "@inquirer/prompts": "^6.0.1", + "@types/commander": "^2.12.2", + "@types/crypto-js": "^4.2.2", + "@types/eccrypto": "^1.1.6", + "argon2": "^0.41.1", + "chalk": "^5.3.0", + "chalk-template": "^1.1.0", + "commander": "^12.1.0", + "crypto": "^1.0.1", + "crypto-js": "^4.2.0", + "dotenv": "^16.4.5", + "eccrypto": "^1.1.6", + "fs": "^0.0.1-security", + "random-words": "^2.0.1" + } +} diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..c30aa1d --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,11 @@ +import { $ } from "bun"; +import data from "../package.json"; + +const { version: ver } = data; + +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 +`; diff --git a/scripts/init.ts b/scripts/init.ts new file mode 100644 index 0000000..dd4547c --- /dev/null +++ b/scripts/init.ts @@ -0,0 +1,22 @@ +import { $ } from "bun"; +import { stringy } from "../src/utils"; +import { randomBytes } from "crypto"; +import chalkTemplate from "chalk-template"; + +const envstring = `// env.ts +const env = { + // Re-run to get a new value + AUTH_KEY: "${stringy(randomBytes(32))}", + + // Re-run to get a new value + AUTH_SALT: "${stringy(randomBytes(64))}", + + // Can be literally anything! + PHRASE: "May the Force be with you!", +} + +export default env; +`; + +await $`echo ${envstring} > env.ts`; +console.log(chalkTemplate`{green env.ts initialized sucessfully!}`); diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..02883d2 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,217 @@ +import { parse, stringify } from "@iarna/toml"; +import { $ } from "bun"; +import { pbkdf2Sync as pbkdf2, randomBytes } from "crypto"; +import { derive, getPublic } from "eccrypto"; +import { readFileSync, writeFileSync } from "fs"; +import { + buffer, + decrypt, + decrypt_file, + ECDH, + encrypt, + encrypt_file, + generate_recovery_phrase, + hmac, + log, + panic, + random, + stringy, +} from "./utils"; +import env from "../env"; +import { AES, enc, HmacSHA256 } from "crypto-js"; +import chalk from "chalk-template"; +import type { ConData, Config, FidData } from "../types"; +import { password } from "@inquirer/prompts"; + +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"); + 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, + }; + 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 recovery_phrase = generate_recovery_phrase(); + writeFileSync("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(), + }, + }; + writeFileSync("config.toml", buffer(stringify(configData), "utf8")); + console.log(`Recovery phrase:`); + console.log(chalk`{magenta ${recovery_phrase}}`); + await $`rm confidant.zip`; + + const gitignore = `# .gitignore + +data.con +key.fid +recovery.txt +confidant.zip +.confidant +${dirname} +.env +`; + writeFileSync(".gitignore", gitignore); +} + +export async function decrypt_diary(password: string) { + // 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.`; + } + + // 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; + + // 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; + + // 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), + ); + await $`unzip confidant.zip > /dev/null && rm confidant.zip`; +} + +export async function encrypt_diary() { + const { dirname, key }: { dirname: string; key: string } = JSON.parse( + decrypt(readFileSync(".confidant"), buffer(env.AUTH_KEY)).toString("utf8"), + ); + const D = buffer(key); + 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); + await $`rm confidant.zip .confidant`; +} + +export async function recovery(recoverystring: string) { + // Check if recovery phrase is correct + let config = Object(parse(readFileSync("config.toml").toString("utf8"))); + let recovery_auth_key = HmacSHA256(recoverystring, env.AUTH_KEY).toString( + enc.Base64, + ); + const dec = AES.decrypt( + config.config.recphrasestore, + recovery_auth_key, + ).toString(enc.Utf8); + if (dec !== env.PHRASE) { + panic`Wrong recovery string. Please try again.`; + } + + // Generate a fresh config file + const auth_secret = decrypt( + buffer(config.config.recoverystore), + buffer(recovery_auth_key), + ); + const newpass = await password({ + message: chalk`{reset {yellow Enter a new password:}}`, + mask: "•", + }); + const recstring = generate_recovery_phrase(); + recovery_auth_key = HmacSHA256(recstring, env.AUTH_KEY).toString(enc.Base64); + const password_auth_key = HmacSHA256(newpass, env.AUTH_KEY).toString( + enc.Base64, + ); + + config.config = { + ...config.config, + 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(config), "utf8")); + + writeFileSync("recovery.txt", recstring + "\n"); + console.log(`New recovery phrase:`); + log`{magenta ${recstring}}`; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..8845a45 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,94 @@ +import { AES, enc } from "crypto-js"; +import { createHmac } from "crypto"; +import { generatePrivate, getPublic } from "eccrypto"; +import { generate } from "random-words"; +import chalk from "chalk-template"; +import { $ } from "bun"; +import { existsSync } from "fs"; + +export function buffer(input: string): Buffer; +export function buffer(input: string, encoding: BufferEncoding): Buffer; +export function buffer(input: string, encoding?: BufferEncoding): Buffer { + return Buffer.from(input, encoding || "base64"); +} + +export function stringy(input: Buffer): string { + return input.toString("base64"); +} + +export function encrypt(input: Buffer, key: Buffer) { + const result = AES.encrypt(stringy(input), stringy(key)); + return buffer(result.toString()); +} + +export function decrypt(input: Buffer, key: Buffer) { + const result = AES.decrypt(stringy(input), stringy(key)); + return buffer(result.toString(enc.Utf8)); +} + +export function random(start: number, end: number) { + return Math.floor((end - start) * Math.random() + start); +} + +export function ECDH() { + const privkey = generatePrivate(); + const pubkey = getPublic(privkey); + return [privkey, pubkey]; +} + +export function hmac(message: Buffer, key: Buffer): Buffer { + return buffer( + createHmac("sha256", stringy(key)) + .update(stringy(message)) + .digest("base64"), + ); +} + +export function generate_recovery_phrase(): string { + return ( + generate({ exactly: 12, minLength: 5, maxLength: 7 }) as string[] + ).join(" "); +} + +export function encrypt_file(input: Buffer, key: Buffer) { + const result = AES.encrypt(input.toString("base64"), key.toString("base64")); + return Buffer.from(result.toString(), "base64"); +} + +export function decrypt_file(input: Buffer, key: Buffer) { + const result = AES.decrypt(input.toString("base64"), key.toString("base64")); + return Buffer.from(result.toString(enc.Utf8), "base64"); +} + +export async function exists(dir: string) { + try { + await $`test -d ${dir}`; + return true; + } catch (e) { + return false; + } +} + +export function log(str: TemplateStringsArray, ...placeholders: unknown[]) { + console.log(chalk(str, placeholders)); +} + +export function panic(str: TemplateStringsArray) { + console.log(chalk`{red ${str}}`); + process.exit(1); +} + +export default function checkForFiles() { + const missing: string[] = []; + ["data.con", "key.fid", "vault.ant", "config.toml"].forEach((val) => { + if (!existsSync(val)) { + missing.push(val); + } + }); + if (missing.length > 0) { + missing.forEach((val) => { + console.log(chalk`{red File "${val}" not found!}`); + }); + process.exit(1); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..1e70316 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,24 @@ +export interface Config { + config: { + con: string; + fid: string; + ant: string; + dir: string; + keystore: string; + recoverystore: string; + phrasestore: string; + recphrasestore: string; + }; +} + +export interface ConData { + privateKey: string; + fidkey: string; + consalt: string; +} + +export interface FidData { + privateKey: string; + salt: string; + code: number; +}