mirror of
https://github.com/silicoflare/confidant.git
synced 2026-05-26 12:45:24 +05:30
feat: new init and decrypt
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -176,3 +176,4 @@ dist
|
||||
|
||||
# confidant
|
||||
env.ts
|
||||
test
|
||||
|
||||
47
confidant.ts
47
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>", "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}`,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"init": "bun ./scripts/init.ts",
|
||||
"sh": "bun ./scripts/sh.ts",
|
||||
"build": "bun ./scripts/build.ts"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
152
src/main.ts
152
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",
|
||||
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`;
|
||||
}
|
||||
|
||||
|
||||
83
src/utils.ts
83
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user