Skip to content
This repository has been archived by the owner on Nov 10, 2022. It is now read-only.

Commit

Permalink
Merge 570466c into 7d21373
Browse files Browse the repository at this point in the history
  • Loading branch information
Alorel committed Sep 17, 2018
2 parents 7d21373 + 570466c commit 94efbac
Show file tree
Hide file tree
Showing 19 changed files with 475 additions and 52 deletions.
2 changes: 1 addition & 1 deletion src/alo.ts
Expand Up @@ -24,6 +24,6 @@ export function alo(args: string | string[]): Promise<string> {
});
}

if (!process.env.RUNNING_PERSONAL_BUILD_TOOLS_TESTS) {
if (!process.env.RUNNING_PERSONAL_BUILD_TOOLS_TESTS || process.env.RUNNING_PERSONAL_BUILD_TOOLS_TESTS_FORCE) {
argv.global('config').parse();
}
61 changes: 49 additions & 12 deletions src/commands/cfg/set.ts
@@ -1,36 +1,73 @@
import * as fs from 'fs';
import * as JSON5 from 'json5';
import {CommandModule} from 'yargs';
import {addCfgKey, addCfgScope, CfgRmConf} from '../../commons/cfg';
import {addCfgKey, addCfgScope, addEncrypt, addPwd, CfgRmConf} from '../../commons/cfg';
import {applyGlobalGroup} from '../../fns/add-cmd/applyGlobalGroup';
import {cmdName} from '../../fns/cmdName';
import {ConfigWriter} from '../../lib/ConfigWriter';
import {Crypt} from '../../lib/Crypt';
import {PromptableConfig} from '../../lib/PromptableConfig';

interface CfgSetConf extends CfgRmConf {
encrypt: boolean;

fromFile: boolean;

password: string;

value: any;
}

function tryJson5Parse(v: any): any {
try {
return JSON5.parse(v);
} catch {
return v;
}
}

const cmd: CommandModule = {
builder(argv) {
addCfgKey(argv);
applyGlobalGroup(argv);
addEncrypt(argv);

argv.option('from-file', {
default: false,
describe: 'Get the value from a file instead of the positional argument. '
+ 'The positional value argument then acts as a filepath.',
type: 'boolean'
});

addPwd(argv);

argv.positional('value', {
coerce(v: any): any {
try {
return JSON5.parse(v);
} catch {
return v;
}
},
describe: 'Config value. Can optionally be a JSON5-parseable item.'
coerce: tryJson5Parse,
describe: 'Config value. Can be a JSON5-parseable item. Optional only if the --stdin option is present.'
});

return addCfgScope(argv);
},
command: `${cmdName(__filename)} <key> <value> [scope]`,
command: `${cmdName(__filename)} <key> [value] [scope]`,
describe: 'Set a config option shared by all projects',
handler(c: CfgSetConf) {
new ConfigWriter().set(c.key, c.value, c.scope).save();
handler(c$: CfgSetConf) {
if (c$.fromFile) {
c$.value = tryJson5Parse(fs.readFileSync(c$.value, 'utf8'));
}

let val: any;

if (c$.encrypt) {
if (typeof c$.value !== 'string') {
throw new Error('Only strings can be encrypted');
}
const c = new PromptableConfig(c$);
val = Crypt.encryptVar(c$.value, c.promptedEncryptionPassword());
} else {
val = c$.value;
}

new ConfigWriter().set(c$.key, val, c$.scope).save();
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/commands/clean-pkg-json.ts
Expand Up @@ -174,7 +174,7 @@ const cmd: CommandModule = {
}

//tslint:disable-next-line:no-magic-numbers
fs.writeFileSync(file, JSON.stringify(contents, null, 2));
fs.writeFileSync(file, JSON.stringify(contents, null, 2).trim() + '\n');
}
}
};
Expand Down
2 changes: 1 addition & 1 deletion src/commands/sort-deps.ts
Expand Up @@ -52,7 +52,7 @@ const cmd: CommandModule = {
}
}

fs.writeFileSync(file, JSON.stringify(contents, null, c.indent));
fs.writeFileSync(file, JSON.stringify(contents, null, c.indent) + '\n');
}
}
};
Expand Down
25 changes: 24 additions & 1 deletion src/commons/cfg.ts
@@ -1,4 +1,5 @@
import {Argv} from 'yargs';
import {Argv, Options} from 'yargs';
import {Nil} from '../interfaces/Nil';

export function addCfgKey<T extends Argv>(argv: T): T {
return <T>argv.positional('key', {
Expand All @@ -7,6 +8,28 @@ export function addCfgKey<T extends Argv>(argv: T): T {
});
}

export function addPwd<T extends Argv>(argv: T): T {
return <T>argv.option('password', {
alias: 'pwd',
describe: 'Encryption password',
type: 'string'
});
}

export function addEncrypt<T extends Argv>(argv: T, alias: string | Nil = 'enc'): T {
const opt: Options = {
default: false,
describe: 'Encrypt the input',
type: 'boolean'
};

if (alias) {
opt.alias = alias;
}

return <T>argv.option('encrypt', opt);
}

export function addCfgScope<T extends Argv>(argv: T): T {
return <T>argv.positional('scope', {
describe: 'Optional scope of the config key (defaults to global)',
Expand Down
3 changes: 2 additions & 1 deletion src/fns/add-cmd/addConfig.ts
Expand Up @@ -7,6 +7,7 @@ import * as YAML from 'yamljs';
import {Argv} from 'yargs';
import {defaultCfgName} from '../../const/defaultCfgName';
import {Group} from '../../inc/Group';
import {parseJointCfgForEncryption} from '../parseJointCfgForEncryption';

function readCfg(p: string): any {
if (/\.js(on)?$/.test(p)) {
Expand Down Expand Up @@ -45,7 +46,7 @@ function loadConfig(key: string): (path: string) => any {
const global = cloneDeep(loadCfgFromPath(join(homedir(), defaultCfgName), key));
const local = cloneDeep(loadCfgFromPath(p, key));

return merge(global, local);
return parseJointCfgForEncryption(merge(global, local));
};
}

Expand Down
73 changes: 73 additions & 0 deletions src/fns/parseJointCfgForEncryption.ts
@@ -0,0 +1,73 @@
import {cloneDeep, forEach} from 'lodash';
import {Colour} from '../lib/Colour';
import {Crypt} from '../lib/Crypt';
import {PromptableConfig} from '../lib/PromptableConfig';

const enum Conf {
MAX_ATTEMPTS = 3
}

function setRegular(cfg: any, k: string, value: any): void {
Object.defineProperty(cfg, k, {
configurable: true,
enumerable: true,
value,
writable: true
});
}

function setVirtual(pcfg: PromptableConfig<any>, cfg: any, k: string, v: any): void {
let attempt = 1;
const colourFns = [<any>undefined, Colour.green, Colour.yellow, Colour.red];

Object.defineProperty(cfg, k, {
configurable: true,
enumerable: true,
get: function getter(): any {
const pwd = pcfg.promptedEncryptionPassword();
try {
const decrypted = Crypt.decryptVar(v, pwd);
setRegular(cfg, k, decrypted);
attempt = 1;

return decrypted;
} catch (e) {
const msg = [
Colour.red(e.message),
` [${colourFns[attempt].call(Colour, attempt)}/`,
`${Colour.red(Conf.MAX_ATTEMPTS.toString())}]\n`
].join('');
process.stderr.write(msg);

if (attempt++ < Conf.MAX_ATTEMPTS) {
delete pcfg['data'].password;
pcfg.promptedEncryptionPassword['cache'].clear();

return getter();
} else {
process.exit(1);
}
}
},
set(v$: any) {
if (Crypt.isEncrypted(v$)) {
setVirtual(pcfg, cfg, k, v$);
} else {
setRegular(cfg, k, v$);
}
}
});
}

export function parseJointCfgForEncryption<T>(cfg: T): T {
cfg = cloneDeep(cfg);
const pcfg = new PromptableConfig(cfg);

forEach(<any>cfg, (v: any, k: string) => {
if (Crypt.isEncrypted(v)) {
setVirtual(pcfg, cfg, k, v);
}
});

return cfg;
}
1 change: 1 addition & 0 deletions src/interfaces/Nil.ts
@@ -0,0 +1 @@
export type Nil = undefined | null;
64 changes: 64 additions & 0 deletions src/lib/Crypt.ts
@@ -0,0 +1,64 @@
import * as crypto from 'crypto';
import {isObject} from 'lodash';

const enum Conf {
IV_RANDOM_BYTES = 16,
SALT_RANDOM_BYTES = 64,
KEY_ITERATIONS = 16384,
KEY_LEN = 32,
CIPHER = 'aes-256-gcm',
HASH = 'sha512',
IV_START = 64,
TAG_START = 80,
TEXT_START = 96
}

export interface Encrypted {
__encrypted: string;
}

export class Crypt {
public static decrypt(encrypted: string, password: string): string {
const bData = Buffer.from(encrypted, 'base64');

const salt = bData.slice(0, Conf.IV_START);
const iv = bData.slice(Conf.IV_START, Conf.TAG_START);
const tag = bData.slice(Conf.TAG_START, Conf.TEXT_START);
const text = bData.slice(Conf.TEXT_START);

const key = crypto.pbkdf2Sync(password, salt, Conf.KEY_ITERATIONS, Conf.KEY_LEN, Conf.HASH);
const decipher = crypto.createDecipheriv(Conf.CIPHER, key, iv);
decipher.setAuthTag(tag);

const out = decipher.update(text, 'binary', 'utf8') + decipher.final('utf8');

if (!out) {
throw new Error('Unable to decrypt');
}

return out;
}

public static decryptVar(v: Encrypted, password: string): string {
return Crypt.decrypt(v.__encrypted, password);
}

public static encrypt(text: string, password: string): string {
const iv = crypto.randomBytes(Conf.IV_RANDOM_BYTES);
const salt = crypto.randomBytes(Conf.SALT_RANDOM_BYTES);
const key = crypto.pbkdf2Sync(password, salt, Conf.KEY_ITERATIONS, Conf.KEY_LEN, Conf.HASH);
const cipher = crypto.createCipheriv(Conf.CIPHER, key, iv);
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();

return Buffer.concat([salt, iv, authTag, encrypted]).toString('base64');
}

public static encryptVar(text: string, password: string): Encrypted {
return {__encrypted: Crypt.encrypt(text, password)};
}

public static isEncrypted(v: any): v is Encrypted {
return !!v && isObject(v) && typeof v.__encrypted === 'string';
}
}
14 changes: 14 additions & 0 deletions src/lib/PromptableConfig.ts
Expand Up @@ -96,6 +96,11 @@ export class PromptableConfig<T extends { [k: string]: any }> {
return this.getPromptEmail(prop, 'What\'s your email? ');
}

@Memo
public promptedEncryptionPassword(prop = 'password'): string {
return this.promptHidden(prop, 'Encryption password: ');
}

@Memo
public promptedGhRepo(prop = 'ghRepo'): string {
let msg = 'What is your GitHub repo';
Expand Down Expand Up @@ -194,4 +199,13 @@ export class PromptableConfig<T extends { [k: string]: any }> {
return this.data[k];
}
}

private promptHidden<K extends keyof T>(k: K, question: string, forbidEmpty = true, strict = true): string {
return this.promptCommon(
k,
() => rl.question(question, {hideEchoBack: true, cancel: true}),
forbidEmpty,
strict
);
}
}

0 comments on commit 94efbac

Please sign in to comment.