add encryption, decryption commands

This commit is contained in:
Alex Bosworth 2020-01-20 21:16:08 -08:00
parent 6ca32cee02
commit ab2af2670c
No known key found for this signature in database
GPG key ID: E80D2F3F311FD87E
11 changed files with 799 additions and 328 deletions

View file

@ -1 +1 @@
exclude = '{$exclude,node_modules}'
exclude = '{$exclude,node_modules,*~}'

View file

@ -1,5 +1,10 @@
# Versions
## Version 5.11.0
- `decrypt`: Add method to decrypt a message using the node key
- `encrypt`: Add method to decrypt a message using the node key
## Version 5.10.1
- `liquidity-cost`: Add parameter to get fast swap out price

42
bos
View file

@ -16,7 +16,9 @@ const request = require('request');
const {accountingCategories} = require('./balances');
const {adjustSavedNodes} = require('./nodes');
const {authenticatedLnd} = require('./lnd');
const {encryptToNode} = require('./encryption');
const {exchanges} = require('./fiat');
const {decryptWithNode} = require('./encryption');
const {findRecord} = require('./lnd');
const {gateway} = require('./lnd');
const {getAccountingReport} = require('./balances');
@ -301,6 +303,46 @@ prog
});
})
// Decrypt a message
.command('decrypt', 'Decrypt data using the node key')
.argument('<encrypted>', 'Encrypted message')
.help('Decrypt a message encrypted to the node key or to another node key')
.option('--node <node_name>', 'Node to decrypt with')
.action((args, options, logger) => {
return new Promise(async (resolve, reject) => {
try {
return decryptWithNode({
encrypted: args.encrypted,
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
},
returnObject({logger, reject, resolve}));
} catch (err) {
return reject(err);
}
});
})
// Encrypt a message
.command('encrypt', 'Encrypt data using the node key')
.help('Encrypt a message to the node key or to another node key')
.option('--node <node_name>', 'Node to encrypt with')
.option('--message <message>', 'Text message to encrypt')
.option('--to <to>', 'Encrypt message to another node')
.action((args, options, logger) => {
return new Promise(async (resolve, reject) => {
try {
return encryptToNode({
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
message: options.message,
to: options.to,
},
returnObject({logger, reject, resolve}));
} catch (err) {
return reject(err);
}
});
})
// Fan out utxos
.command('fanout', 'Fan out utxos')
.argument('<size>', 'UTXO minimum size', INT)

View file

@ -0,0 +1,134 @@
const asyncAuto = require('async/auto');
const {decodeFirst} = require('cbor');
const {diffieHellmanComputeSecret} = require('ln-service');
const {getNode} = require('ln-service');
const {getWalletInfo} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const decryptWithSecret = require('./decrypt_with_secret');
const hexAsBuf = hex => Buffer.from(hex, 'hex');
const {isArray} = Array;
/** Decrypt data from node
{
encrypted: <Encrypted Data Hex String>
lnd: <Authenticated LND gRPC API Object>
}
@returns via cbk or Promise
{
message: <Message Object>
with_alias: <With Node Public Key Hex String>
with_public_key: <With Public Key Hex String>
}
*/
module.exports = ({encrypted, lnd}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!encrypted) {
return cbk([400, 'ExpectedEncryptedPayloadToDecryptData']);
}
if (!lnd) {
return cbk([400, 'ExpectedLndToDecryptData']);
}
return cbk();
},
// Decode encrypted CBOR
decode: ['validate', ({}, cbk) => {
return decodeFirst(encrypted, (err, details) => {
if (!!err) {
return cbk([400, 'ExpectedCborEncodedEncryptedData', {err}]);
}
if (!details) {
return cbk([400, 'ExpectedDecodedDetailsToDecryptWithNode']);
}
if (!details.settings) {
return cbk([400, 'ExpectedEncryptionSettingsToDecryptWithNode']);
}
if (!isArray(details.settings.with)) {
return cbk([400, 'ExpectedWithNodesPublicKeyToDecrypt']);
}
return cbk(null, details);
});
}],
// Get node public key
getPublicKey: ['decode', ({decode}, cbk) => {
return getWalletInfo({lnd}, cbk);
}],
// Determine the foreign key if any
foreignKey: ['decode', 'getPublicKey', ({decode, getPublicKey}, cbk) => {
const keys = decode.settings.with;
const foreignKey = keys.find(n => n !== getPublicKey.public_key);
return cbk(null, foreignKey || getPublicKey.public_key);
}],
// Get details about the node
getNode: ['foreignKey', ({foreignKey}, cbk) => {
return getNode({lnd, public_key: foreignKey}, (err, res) => {
if (!!err) {
return cbk(null, {public_key: foreignKey});
}
return cbk(null, {alias: res.alias, public_key: foreignKey});
});
}],
// Derive the shared secret
getSecret: ['foreignKey', ({foreignKey}, cbk) => {
return diffieHellmanComputeSecret({
lnd,
partner_public_key: foreignKey,
},
cbk);
}],
// Decrypt the data
decrypt: ['decode', 'getSecret', ({decode, getSecret}, cbk) => {
return decryptWithSecret({
encrypted: Buffer.from(decode.encrypted, 'hex'),
iv: decode.iv,
salt: decode.salt,
secret: getSecret.secret,
settings: {
algorithm: decode.settings.algorithm,
derivation: decode.settings.derivation,
digest: decode.settings.digest,
key_length: decode.settings.key_length,
},
tag: decode.tag,
},
cbk);
}],
// Cleartext
clear: [
'decode',
'decrypt',
'getNode',
({decode, decrypt, getNode}, cbk) =>
{
return cbk(null, {
with_alias: getNode.alias || undefined,
with_public_key: getNode.public_key,
message: hexAsBuf(decrypt.plain).toString(decode.settings.encoding),
});
}],
},
returnResult({reject, resolve, of: 'clear'}, cbk));
});
};

View file

@ -0,0 +1,113 @@
const {createDecipheriv} = require('crypto');
const {scrypt} = require('crypto');
const asyncAuto = require('async/auto');
const {returnResult} = require('asyncjs-util');
const algorithm = 'aes-256-gcm';
const bufAsHex = n => n.toString('hex');
const derivation = 'scrypt';
const digest = 'sha512';
const fromHex = n => Buffer.from(n, 'hex');
const keyLength = 32;
/** Decrypt encrypted data with a secret
{
encrypted: <Encrypted Data Buffer Object>
iv: <Initialization Vector Buffer Object>
salt: <Salt Buffer Object>
secret: <Secret Hex String>
settings: {
algorithm: <Algorithm String>
derivation: <Key Derivation Type String>
digest: <Digest String>
key_length: <Key Length Number>
}
tag: <Auth Tag Buffer Object>
}
@returns via cbk or Promise
{
plain: <Decrypted Data Hex String>
}
*/
module.exports = ({encrypted, iv, salt, secret, settings, tag}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!Buffer.isBuffer(encrypted)) {
return cbk([400, 'ExpectedEncryptedDataToDecryptWithSecret']);
}
if (!Buffer.isBuffer(iv)) {
return cbk([400, 'ExpectedIvToDecryptWithSecret']);
}
if (!Buffer.isBuffer(salt)) {
return cbk([400, 'ExpectedSaltToDecryptWithSecret']);
}
if (!secret) {
return cbk([400, 'ExpectedSecretToDecryptWithSecret']);
}
if (!settings) {
return cbk([400, 'ExpectedSettingsToDecryptWithSecret']);
}
if (settings.algorithm !== algorithm) {
return cbk([400, 'ExpectedKnownAlgorithmToDecryptWithSecret']);
}
if (settings.derivation !== derivation) {
return cbk([400, 'ExpectedKnownKeyDerivationToDecryptWithSecret']);
}
if (settings.digest !== digest) {
return cbk([400, 'ExpectedKnownDigestToDecryptWithSecret']);
}
if (settings.key_length !== keyLength) {
return cbk([400, 'ExpectedKnownKeyLengthToDecryptWithSecret']);
}
if (!tag) {
return cbk([400, 'ExpectedAuthTagToDecryptWithSecret']);
}
return cbk();
},
// Derive the key from the secret
key: ['validate', ({}, cbk) => {
const keyLength = settings.key_length;
return scrypt(fromHex(secret), salt, keyLength, (err, derivedKey) => {
if (!!err) {
return cbk([503, 'UnexpectedErrorGettingDerivativeKey', {err}]);
}
return cbk(null, derivedKey);
});
}],
// Decrypt data with the derived key
decrypt: ['key', ({key}, cbk) => {
const decipher = createDecipheriv(settings.algorithm, key, iv);
decipher.setAuthTag(tag);
try {
const elements = [decipher.update(encrypted), decipher.final()];
return cbk(null, {plain: bufAsHex(Buffer.concat(elements))});
} catch (err) {
return cbk([503, 'FailedToDecryptMessage', {err}]);
}
}],
},
returnResult({reject, resolve, of: 'decrypt'}, cbk));
});
};

View file

@ -0,0 +1,92 @@
const asyncAuto = require('async/auto');
const {diffieHellmanComputeSecret} = require('ln-service');
const {getWalletInfo} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const encryptToSecret = require('./encrypt_to_secret');
/** Encrypt data to a node
{
lnd: <Authenticated LND gRPC API Object>
message: <Message to Encrypt String>
[to]: <Encrypt to Public Key Hex String>
}
@returns via cbk or Promise
{
encrypted: <Encrypted Data String>
to: <Encrypted To Node Hex String>
}
*/
module.exports = ({lnd, message, to}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!lnd) {
return cbk([400, 'ExpectedLndToEncryptToNode']);
}
if (!message) {
return cbk([400, 'ExpectedMessageToEncryptToNode']);
}
return cbk();
},
// Get own public key
getPublicKey: ['validate', ({}, cbk) => {
return getWalletInfo({lnd}, cbk);
}],
// Encode to public key
toPublicKey: ['getPublicKey', ({getPublicKey}, cbk) => {
if (!!to) {
return cbk(null, {public_key: to});
}
return cbk(null, {public_key: getPublicKey.public_key});
}],
// Get the secret
getSecret: ['toPublicKey', ({toPublicKey}, cbk) => {
return diffieHellmanComputeSecret({
lnd,
partner_public_key: toPublicKey.public_key,
},
cbk);
}],
// Encrypt to the secret
encryptToSecret: [
'getPublicKey',
'getSecret',
'toPublicKey',
({getPublicKey, getSecret, toPublicKey}, cbk) =>
{
return encryptToSecret({
encoding: 'utf8',
from: getPublicKey.public_key,
plain: Buffer.from(message, 'utf8').toString('hex'),
secret: getSecret.secret,
to: toPublicKey.public_key,
},
cbk);
}],
// Encrypted
encrypted: [
'encryptToSecret',
'toPublicKey',
({encryptToSecret, toPublicKey}, cbk) =>
{
return cbk(null, {
encrypted: encryptToSecret.cipher,
to: toPublicKey.public_key,
});
}],
},
returnResult({reject, resolve, of: 'encrypted'}, cbk));
});
};

View file

@ -0,0 +1,98 @@
const {createCipheriv} = require('crypto');
const {randomBytes} = require('crypto');
const {scrypt} = require('crypto');
const asyncAuto = require('async/auto');
const {encode} = require('cbor');
const {returnResult} = require('asyncjs-util');
const algorithm = 'aes-256-gcm';
const cbor = n => encode(n).toString('hex');
const derivation = 'scrypt';
const digest = 'sha512';
const fromHex = n => Buffer.from(n, 'hex');
const ivByteLength = 16;
const keyLength = 32;
const saltByteLength = 32;
const uniq = arr => Array.from(new Set(arr));
/** Encrypt data to a secret
{
[encoding]: <Data Encoding Format String>
[from]: <Data is From Public Key Hex String>
plain: <Data To Encrypt Hex String>
secret: <Encryption Secret Hex String>
[to]: <Data is To Public Key Hex String>
}
@returns via cbk or Promise
{
cipher: <CBOR Encoded Encrypted Hex String>
}
*/
module.exports = ({encoding, from, plain, secret, to}, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!plain) {
return cbk([400, 'ExectedDataToEncryptToSecret']);
}
if (!secret) {
return cbk([400, 'ExpectedSecretToEncryptToSecret']);
}
return cbk();
},
// Generate salt
salt: ['validate', ({}, cbk) => cbk(null, randomBytes(saltByteLength))],
// Encryption settings
settings: ['validate', ({}, cbk) => {
return cbk(null, {
algorithm,
derivation,
digest,
encoding,
key_length: keyLength,
with: uniq([from, to]),
});
}],
// Generate a derivative key
key: ['salt', ({salt}, cbk) => {
return scrypt(fromHex(secret), salt, keyLength, (err, derivedKey) => {
if (!!err) {
return cbk([503, 'UnexpectedErrorGeneratingDerivativeKey', {err}]);
}
return cbk(null, derivedKey);
});
}],
// Encrypt to derivative key
encrypt: ['key', 'salt', 'settings', ({key, salt, settings}, cbk) => {
const iv = randomBytes(ivByteLength);
try {
createCipheriv(algorithm, key, iv);
} catch (err) {
return cbk([500, 'FailedToCreateCipherWhenEncryptingToSecret']);
}
const cipher = createCipheriv(algorithm, key, iv);
const updated = [cipher.update(fromHex(plain)), cipher.final()];
const encrypted = Buffer.concat(updated);
const tag = cipher.getAuthTag();
return cbk(null, {cipher: cbor({encrypted, iv, salt, settings, tag})});
}],
},
returnResult({reject, resolve, of: 'encrypt'}, cbk));
});
};

View file

@ -1,13 +1,17 @@
const decryptCiphertext = require('./decrypt_ciphertext');
const decryptPayload = require('./decrypt_payload');
const decryptWithNode = require('./decrypt_with_node');
const derAsPem = require('./der_as_pem');
const encryptToNode = require('./encrypt_to_node');
const encryptToPublicKeys = require('./encrypt_to_public_keys');
const pemAsDer = require('./pem_as_der');
module.exports = {
decryptCiphertext,
decryptPayload,
decryptWithNode,
derAsPem,
encryptToNode,
encryptToPublicKeys,
pemAsDer,
};

627
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -26,8 +26,8 @@
"html2unicode": "1.1.3",
"ini": "1.3.5",
"inquirer": "7.0.3",
"ln-accounting": "3.1.7",
"ln-service": "47.10.4",
"ln-accounting": "3.1.8",
"ln-service": "47.12.0",
"moment": "2.24.0",
"qrcode-terminal": "0.12.0",
"request": "2.88.0",
@ -39,7 +39,7 @@
},
"description": "Lightning balance CLI",
"devDependencies": {
"tap": "14.10.5"
"tap": "14.10.6"
},
"engines": {
"node": ">=10.12.0"
@ -62,5 +62,5 @@
"scripts": {
"test": "tap test/arrays/*.js test/balances/*.js test/chain/*.js test/encryption/*.js test/fiat/*.js test/lnd/*.js test/network/*.js test/nodes/*.js test/responses/*.js test/routing/*.js test/swaps/*.js test/telegram/*.js"
},
"version": "5.10.1"
"version": "5.11.0"
}

View file

@ -907,7 +907,7 @@ module.exports = (args, cbk) => {
min_fee_rate: minFeeRate,
private_key: claim.private_key,
secret: claim.secret,
start_height: initiateSwap.start_height || depositHeight,
start_height: depositHeight || initiateSwap.start_height,
sweep_address: createAddress.address,
transaction_id: claim.transaction_id,
transaction_vout: claim.transaction_vout,