Skip to content

Commit

Permalink
Merge pull request #301 from LiskHQ/267_move_core_specific_commands
Browse files Browse the repository at this point in the history
Move core specific commands - Closes #267
  • Loading branch information
shuse2 committed Jul 24, 2020
2 parents 293ee81 + d6400be commit 9ddb4b2
Show file tree
Hide file tree
Showing 11 changed files with 869 additions and 76 deletions.
240 changes: 167 additions & 73 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -59,10 +59,12 @@
}
},
"dependencies": {
"@liskhq/lisk-passphrase": "3.0.0",
"@oclif/command": "1.6.1",
"@oclif/config": "1.15.1",
"@oclif/plugin-help": "3.1.0",
"fs-extra": "9.0.1",
"inquirer": "7.3.2",
"lisk-sdk": "5.0.0-alpha.2",
"tar": "6.0.2",
"tslib": "1.13.0",
Expand Down
86 changes: 86 additions & 0 deletions src/commands/hash-onion.ts
@@ -0,0 +1,86 @@
/*
* Copyright © 2020 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*
*/

import { cryptography } from 'lisk-sdk';
import { isValidInteger } from '@liskhq/lisk-validator';
import Command, { flags as flagParser } from '@oclif/command';
import * as fs from 'fs-extra';
import * as path from 'path';

export default class HashOnionCommand extends Command {
static description = `
Creates hash onion output to be used by forger.
`;

static examples = ['hash-onion --count=1000000 --distance=2000'];

static flags = {
output: flagParser.string({
char: 'o',
description: 'Output file path',
}),
count: flagParser.integer({
char: 'c',
description: 'Total number of hashes to produce',
default: 1000000,
}),
distance: flagParser.integer({
char: 'd',
description: 'Distance between each hashes',
default: 1000,
}),
};

// eslint-disable-next-line @typescript-eslint/require-await
async run(): Promise<void> {
const {
flags: { output, count, distance },
} = this.parse(HashOnionCommand);

if (distance <= 0 || !isValidInteger(distance)) {
throw new Error('Invalid distance. Distance has to be positive integer');
}

if (count <= 0 || !isValidInteger(count)) {
throw new Error('Invalid count. Count has to be positive integer');
}

if (output) {
const { dir } = path.parse(output);
fs.ensureDirSync(dir);
}

const seed = cryptography.generateHashOnionSeed();

const hashBuffers = cryptography.hashOnion(seed, count, distance);
const hashes = hashBuffers.map(buf => buf.toString('base64'));

const result = { count, distance, hashes };

if (output) {
fs.writeJSONSync(output, result);
} else {
this.printJSON(result);
}
}

public printJSON(message?: object, pretty = false): void {
if (pretty) {
this.log(JSON.stringify(message, undefined, ' '));
} else {
this.log(JSON.stringify(message));
}
}
}
75 changes: 75 additions & 0 deletions src/commands/passphrase/decrypt.ts
@@ -0,0 +1,75 @@
/*
* Copyright © 2020 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*
*/
import { cryptography } from 'lisk-sdk';
import Command, { flags as flagParser } from '@oclif/command';

import { flags as commonFlags } from '../../utils/flags';
import { getPassphraseFromPrompt } from '../../utils/reader';

interface Args {
readonly encryptedPassphrase?: string;
}

const processInputs = (password: string, encryptedPassphrase: string) => {
const encryptedPassphraseObject = cryptography.parseEncryptedPassphrase(encryptedPassphrase);
const passphrase = cryptography.decryptPassphraseWithPassword(encryptedPassphraseObject, password);

return { passphrase };
};

export default class DecryptCommand extends Command {
static args = [
{
name: 'encryptedPassphrase',
description: 'Encrypted passphrase to decrypt.',
required: true,
},
];

static description = `
Decrypts your secret passphrase using the password which was provided at the time of encryption.
`;

static examples = [
'passphrase:decrypt "iterations=1000000&cipherText=9b1c60&iv=5c8843f52ed3c0f2aa0086b0&salt=2240b7f1aa9c899894e528cf5b600e9c&tag=23c01112134317a63bcf3d41ea74e83b&version=1"',
];

static flags = {
password: flagParser.string(commonFlags.password),
};

async run(): Promise<void> {
const {
args,
flags: { password: passwordSource },
} = this.parse(DecryptCommand);

const { encryptedPassphrase }: Args = args;

const password = passwordSource ?? (await getPassphraseFromPrompt('password', true));

const result = processInputs(password, encryptedPassphrase as string);

this.printJSON(result);
}

public printJSON(message?: object, pretty = false): void {
if (pretty) {
this.log(JSON.stringify(message, undefined, ' '));
} else {
this.log(JSON.stringify(message));
}
}
}
70 changes: 70 additions & 0 deletions src/commands/passphrase/encrypt.ts
@@ -0,0 +1,70 @@
/*
* Copyright © 2020 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*
*/
import { cryptography } from 'lisk-sdk';
import { flags as flagParser, Command } from '@oclif/command';

import { flags as commonFlags } from '../../utils/flags';
import { getPassphraseFromPrompt } from '../../utils/reader';

const outputPublicKeyOptionDescription =
'Includes the public key in the output. This option is provided for the convenience of node operators.';

const processInputs = (passphrase: string, password: string, outputPublicKey: boolean) => {
const encryptedPassphraseObject = cryptography.encryptPassphraseWithPassword(passphrase, password);
const encryptedPassphrase = cryptography.stringifyEncryptedPassphrase(encryptedPassphraseObject);

return outputPublicKey
? {
encryptedPassphrase,
publicKey: cryptography.getKeys(passphrase).publicKey.toString('base64'),
}
: { encryptedPassphrase };
};

export default class EncryptCommand extends Command {
static description = `
Encrypts your secret passphrase under a password.
`;

static examples = ['passphrase:encrypt'];

static flags = {
password: flagParser.string(commonFlags.password),
passphrase: flagParser.string(commonFlags.passphrase),
outputPublicKey: flagParser.boolean({
description: outputPublicKeyOptionDescription,
}),
};

async run(): Promise<void> {
const {
flags: { passphrase: passphraseSource, password: passwordSource, outputPublicKey },
} = this.parse(EncryptCommand);

const passphrase = passphraseSource ?? (await getPassphraseFromPrompt('passphrase', true));
const password = passwordSource ?? (await getPassphraseFromPrompt('password', true));
const result = processInputs(passphrase, password, outputPublicKey);

this.printJSON(result);
}

public printJSON(message?: object, pretty = false): void {
if (pretty) {
this.log(JSON.stringify(message, undefined, ' '));
} else {
this.log(JSON.stringify(message));
}
}
}
30 changes: 30 additions & 0 deletions src/utils/error.ts
@@ -0,0 +1,30 @@
/*
* Copyright © 2020 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*
*/
// eslint-disable-next-line max-classes-per-file
export class FileSystemError extends Error {
public constructor(message: string) {
super(message);
this.message = message;
this.name = 'FileSystemError';
}
}

export class ValidationError extends Error {
public constructor(message: string) {
super(message);
this.message = message;
this.name = 'ValidationError';
}
}
20 changes: 17 additions & 3 deletions src/utils/flags.ts
Expand Up @@ -13,6 +13,16 @@
*
*/

const passphraseDescription = `Specifies a source for your secret passphrase. Command will prompt you for input if this option is not set.
Examples:
- --passphrase='my secret passphrase' (should only be used where security is not important)
`;

const passwordDescription = `Specifies a source for your secret password. Command will prompt you for input if this option is not set.
Examples:
- --password=pass:password123 (should only be used where security is not important)
`;

export type AlphabetLowercase =
| 'a'
| 'b'
Expand Down Expand Up @@ -49,8 +59,12 @@ export interface FlagMap {
}

export const flags: FlagMap = {
dataPath: {
char: 'd',
description: 'Directory path to specify where node data is stored. Environment variable "LISK_DATA_PATH" can also be used.',
passphrase: {
char: 'p',
description: passphraseDescription,
},
password: {
char: 'w',
description: passwordDescription,
},
};
81 changes: 81 additions & 0 deletions src/utils/reader.ts
@@ -0,0 +1,81 @@
/*
* Copyright © 2020 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*
*/

import * as liskPassphrase from '@liskhq/lisk-passphrase';
import * as inquirer from 'inquirer';

import { ValidationError } from './error';

interface MnemonicError {
readonly code: string;
readonly message: string;
}

const capitalise = (text: string): string => `${text.charAt(0).toUpperCase()}${text.slice(1)}`;

const getPassphraseVerificationFailError = (displayName: string): string =>
`${capitalise(displayName)} was not successfully repeated.`;


export const getPassphraseFromPrompt = async (
displayName = 'passphrase',
shouldConfirm = false,
): Promise<string> => {
const questions = [
{
type: 'password',
name: 'passphrase',
message: `Please enter ${displayName}: `,
},
];
if (shouldConfirm) {
questions.push({
type: 'password',
name: 'passphraseRepeat',
message: `Please re-enter ${displayName}: `,
});
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { passphrase, passphraseRepeat } = await inquirer.prompt(questions);

if (!passphrase || (shouldConfirm && passphrase !== passphraseRepeat)) {
throw new ValidationError(getPassphraseVerificationFailError(displayName));
}

const passphraseErrors = [passphrase]
.filter(Boolean)
.map(pass =>
liskPassphrase.validation
.getPassphraseValidationErrors(pass as string)
.filter((error: MnemonicError) => error.message),
);

passphraseErrors.forEach(errors => {
if (errors.length > 0) {
const passphraseWarning = errors
.filter((error: MnemonicError) => error.code !== 'INVALID_MNEMONIC')
.reduce(
(accumulator: string, error: MnemonicError) =>
accumulator.concat(`${error.message.replace(' Please check the passphrase.', '')} `),
'Warning: ',
);
console.warn(passphraseWarning);
}
});

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return passphrase;
};

0 comments on commit 9ddb4b2

Please sign in to comment.