feat: new init and decrypt

This commit is contained in:
2024-09-29 21:40:14 +05:30
parent 2881d9b860
commit 79810d7e03
5 changed files with 164 additions and 130 deletions

1
.gitignore vendored
View File

@@ -176,3 +176,4 @@ dist
# confidant # confidant
env.ts env.ts
test

View File

@@ -1,12 +1,17 @@
#!/bin/env bun #!/bin/env bun
import { input, password, select } from "@inquirer/prompts"; import { checkbox, input, password, select } from "@inquirer/prompts";
import { $ } from "bun"; import { $ } from "bun";
import chalk from "chalk-template"; import chalk from "chalk-template";
import { Command } from "commander"; import { Command } from "commander";
import { decrypt_diary, encrypt_diary, initialize, recovery } from "./src/main"; import { decrypt_diary, encrypt_diary, initialize, recovery } from "./src/main";
import { existsSync } from "fs"; 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 program = new Command();
const { exit } = process; const { exit } = process;
@@ -18,33 +23,8 @@ program
.command("init") .command("init")
.description("initialize a confidant vault") .description("initialize a confidant vault")
.action(async () => { .action(async () => {
// check if a vault already exists const dirname = (await getDirectoryNames()) as string;
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({ const pass = await password({
message: chalk`{reset {yellow Enter a password to use:}}`, message: chalk`{reset {yellow Enter a password to use:}}`,
mask: "•", mask: "•",
@@ -54,8 +34,7 @@ program
mask: "•", mask: "•",
}); });
if (pass !== confpass) { if (pass !== confpass) {
console.log(chalk`Passwords don't math.`); panic`Passwords don't match. Exiting...`;
exit(1);
} }
await initialize(pass, dirname); await initialize(pass, dirname);
@@ -66,14 +45,18 @@ program
.command("decrypt") .command("decrypt")
.description("decrypt the vault") .description("decrypt the vault")
.option("-l, --live", "decrypt in live mode") .option("-l, --live", "decrypt in live mode")
.option("-v, --vault <name>", "name of the vault to decrypt")
.action(async (args) => { .action(async (args) => {
checkForFiles(); let files: string[];
if (!args.vault) {
args.vault = await getVaultName();
}
const pass = await password({ const pass = await password({
message: chalk`{reset {yellow Enter the password:}}`, message: chalk`{reset {yellow Enter the password:}}`,
mask: "•", mask: "•",
}); });
await decrypt_diary(pass); await decrypt_diary(pass, args.vault);
if (args.live) { if (args.live) {
await input({ await input({
message: chalk`{yellow Live mode started. Press ENTER to encrypt}`, message: chalk`{yellow Live mode started. Press ENTER to encrypt}`,

View File

@@ -5,6 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"init": "bun ./scripts/init.ts", "init": "bun ./scripts/init.ts",
"sh": "bun ./scripts/sh.ts",
"build": "bun ./scripts/build.ts" "build": "bun ./scripts/build.ts"
}, },
"bin": { "bin": {

View File

@@ -14,7 +14,9 @@ import {
hmac, hmac,
log, log,
panic, panic,
print,
random, random,
separator,
stringy, stringy,
} from "./utils"; } from "./utils";
import env from "../env"; import env from "../env";
@@ -27,136 +29,104 @@ console.info = function () {};
const { exit } = process; const { exit } = process;
export async function initialize(password: string, dirname: string) { export async function initialize(password: string, dirname: string) {
// compressing and encrypting diary // compressing and encrypting vault
const [K_C, P_C] = ECDH(); const [K_A, P_A] = ECDH();
const [K_F, P_F] = ECDH(); const [K_B, P_B] = ECDH();
const S_CF = await derive(K_C, P_F); const S_AB = await derive(K_A, P_B);
const fidsalt = randomBytes(32); const salt = randomBytes(32);
const fidcode = random(10000, 100000); const code = random(10000, 100000);
const D = pbkdf2(S_CF, fidsalt, fidcode, 64, "sha256"); const D = pbkdf2(S_AB, salt, code, 64, "sha256");
await $`zip -r9 confidant.zip ${dirname} > /dev/null`; await $`zip -r9 confidant.zip ${dirname} > /dev/null`;
await $`rm -rf ${dirname}`; await $`rm -rf ${dirname}`;
const Z = readFileSync("confidant.zip"); const Z = readFileSync("confidant.zip");
const E_Z = encrypt_file(Z, D); const E_Z = encrypt_file(Z, D);
writeFileSync(`vault.ant`, E_Z);
// creating key.fid // creating dirname.key
const fidData = { const keyfile = {
privateKey: stringy(K_F), salt: stringy(salt),
salt: stringy(fidsalt), code: code,
code: fidcode, 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 auth_secret = randomBytes(32);
const conData = { const confsalt = randomBytes(64);
privateKey: stringy(K_C), const K = hmac(auth_secret, confsalt);
fidkey: stringy(fidkey), const E_K = encrypt_file(Buffer.from(JSON.stringify(keyfile), "utf8"), K);
consalt: stringy(consalt), writeFileSync(`${dirname}.key`, E_K);
};
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);
// creating config data
const recovery_phrase = generate_recovery_phrase(); 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( const recovery_auth_key = HmacSHA256(recovery_phrase, env.AUTH_KEY).toString(
enc.Base64, enc.Base64,
); );
const password_auth_key = HmacSHA256(password, env.AUTH_KEY).toString( const password_auth_key = HmacSHA256(password, env.AUTH_KEY).toString(
enc.Base64, enc.Base64,
); );
const D_C = {
// create config.toml dir: dirname,
const configData = { confsalt: stringy(confsalt),
config: { privateKey: stringy(K_B),
con: "data.con", keystore: stringy(encrypt(auth_secret, buffer(password_auth_key))),
fid: "key.fid", recoverystore: stringy(encrypt(auth_secret, buffer(recovery_auth_key))),
ant: "vault.ant", phrasestore: AES.encrypt(env.PHRASE, password_auth_key).toString(),
dir: dirname, recphrasestore: AES.encrypt(env.PHRASE, recovery_auth_key).toString(),
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(`Recovery phrase:`);
console.log(chalk`{magenta ${recovery_phrase}}`); console.log(chalk`{magenta ${recovery_phrase}}`);
await $`rm confidant.zip`; await $`rm confidant.zip`;
// create .gitignore
const gitignore = `# .gitignore const gitignore = `# .gitignore
data.con *.con
key.fid *.fid
recovery.txt *_recovery.txt
confidant.zip confidant.zip
.confidant *.confidant
${dirname}
.env
`; `;
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 // 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( const password_auth_key = HmacSHA256(password, env.AUTH_KEY).toString(
enc.Base64, enc.Base64,
); );
const dec = AES.decrypt( const config = JSON.parse(D_C);
config.config.phrasestore, const phrase = AES.decrypt(config.phrasestore, password_auth_key).toString(
password_auth_key, enc.Utf8,
).toString(enc.Utf8); );
if (dec !== env.PHRASE) { if (phrase !== env.PHRASE) {
panic`Wrong password. Try again or reset it.`; panic`Incorrect password. Try again or reset it.`;
} }
// Decrypt data.con const auth_secret = decrypt(config.keystore, buffer(password_auth_key));
const auth_secret = decrypt( const K = hmac(auth_secret, config.confsalt);
buffer(config.config.keystore), const E_K = readFileSync(`${dirname}.key`);
buffer(password_auth_key), const keyfile = JSON.parse(decrypt_file(E_K, K).toString("utf8"));
);
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 K_A = buffer(keyfile.privateKey);
const { consalt, fidkey, privateKey: conK } = conData; const K_B = buffer(config.privateKey);
const D_C = hmac(buffer(consalt), buffer(fidkey)); const P_B = getPublic(K_B);
const fidData = Object( const S_AB = await derive(K_A, P_B);
parse(decrypt_file(readFileSync(config.config.fid), D_C).toString("utf8")), const D = pbkdf2(S_AB, buffer(keyfile.salt), keyfile.code, 64, "sha256");
) as FidData;
const { privateKey: fidK, salt, code } = fidData;
// Decrypt diary.ant const Z = decrypt_file(E_Z, D);
const S_CF = await derive(buffer(conK), getPublic(buffer(fidK))); writeFileSync(`confidant.zip`, Z);
const D = pbkdf2(S_CF, buffer(salt), code, 64, "sha256"); writeFileSync(`.${dirname}.confidant`, encrypt(D, buffer(env.AUTH_KEY)));
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`; await $`unzip confidant.zip > /dev/null && rm confidant.zip`;
} }

View File

@@ -5,6 +5,7 @@ import { generate } from "random-words";
import chalk from "chalk-template"; import chalk from "chalk-template";
import { $ } from "bun"; import { $ } from "bun";
import { existsSync } from "fs"; import { existsSync } from "fs";
import { select } from "@inquirer/prompts";
export function buffer(input: string): Buffer; export function buffer(input: string): Buffer;
export function buffer(input: string, encoding: BufferEncoding): 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[]) { export function log(
console.log(chalk(str, placeholders)); str: TemplateStringsArray,
...placeholders: unknown[]
): void {
console.log(chalk(str as TemplateStringsArray, ...placeholders));
} }
export function panic(str: TemplateStringsArray) { export function panic(str: TemplateStringsArray) {
@@ -92,3 +96,78 @@ export default function checkForFiles() {
process.exit(1); 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;
}