yaml-crypt
Advanced tools
Comparing version 0.2.0 to 0.3.0
@@ -119,12 +119,15 @@ #!/usr/bin/env node | ||
}); | ||
parser.addArgument(['-k', '--key'], { | ||
action: 'append', | ||
metavar: '<key-file>', | ||
help: 'Read a key from the given file path. Can be given multiple times to automatically select a matching decryption key' | ||
parser.addArgument(['--generate-key'], { | ||
action: 'storeTrue', | ||
help: 'Generate a new random key. Use -a to specify the algorithm' | ||
}); | ||
parser.addArgument(['--key-fd'], { | ||
parser.addArgument(['-k'], { | ||
action: 'append', | ||
metavar: '<key-fd>', | ||
help: 'Read a key from the given file descriptor. Can be given multiple times' | ||
metavar: '<key>', | ||
help: 'Use the given key to decrypt data. Can be given multiple times. See section "Key sources" for details' | ||
}); | ||
parser.addArgument(['-K'], { | ||
metavar: '<key>', | ||
help: 'Use the given key to encrypt data. See section "Key sources" for details' | ||
}); | ||
parser.addArgument(['-a', '--algorithm'], { | ||
@@ -154,5 +157,5 @@ metavar: '<algorithm>', | ||
}); | ||
parser.addArgument(['--rm'], { | ||
parser.addArgument(['--keep'], { | ||
action: 'storeTrue', | ||
help: 'Delete original files after encryption/decryption. Use with caution!' | ||
help: 'Keep the original files after encryption/decryption' | ||
}); | ||
@@ -164,2 +167,38 @@ parser.addArgument(['file'], { | ||
}); | ||
if (process.argv && process.argv.includes('--help')) { | ||
parser.addArgumentGroup({ | ||
title: 'Configuration file', | ||
description: 'During startup, yaml-crypt will look for a configuration file ' | ||
+ '"config.yaml" or "config.yml" in the folder "$HOME/.yaml-crypt" and read keys from ' | ||
+ 'the array "keys". Each key is expected to be an object with the required ' | ||
+ 'attribute "key" which contains the raw key data and an optional attribute ' | ||
+ '"name" with a custom name for that key.' | ||
}); | ||
parser.addArgumentGroup({ | ||
title: 'Key sources', | ||
description: 'Keys can be provided from multiple sources: configuration file, environment variables, key files and file descriptors. ' | ||
+ 'When no explicit specifier is given, any arguments will be treated as key files. To select a specific source, ' | ||
+ 'specify "c:" or "config:" for configuration file, "e:" or "env:" for environment variables, "fd:" for file descriptors and "f:" for files. ' | ||
+ 'For example, "yaml-crypt -k c:my-key -k e:MY_KEY -k fd:0 -k f:my.key" will read the key named "my-key" from ' | ||
+ 'the configuration file, read a key from the environment variable "MY_KEY", read another key from file descriptor 0 (stdin) and another key from ' | ||
+ 'the local file "my.key".' | ||
}); | ||
parser.addArgumentGroup({ | ||
title: 'Decryption keys', | ||
description: 'When no keys are given, decryption keys are read from the configuration file. ' | ||
+ 'When no decryption keys are given, but an encryption key is given, that key will also be ' | ||
+ 'used for decryption. ' | ||
+ 'All provided decryption keys are tried, in order, until the data can be successfully decrypted. ' | ||
+ 'If none of the available keys matches, the operation fails.' | ||
}); | ||
parser.addArgumentGroup({ | ||
title: 'Encryption keys', | ||
description: 'When no encryption key is given and only one decryption key is available, that ' | ||
+ 'key will be used for encryption. When editing a file and no encryption key is given, ' | ||
+ 'the matching decryption key will be used to encrypt the modified data. ' | ||
+ 'In all other cases, an encryption key must be explicitly selected using "-K".' | ||
}); | ||
} else { | ||
parser.epilog = 'For more details, specify --help'; | ||
} | ||
const args = parser.parseArgs(argv); | ||
@@ -170,3 +209,3 @@ if (args.encrypt && args.decrypt) { | ||
if (args.raw && args.path) { | ||
throw new UsageError('options --raw and --path cannot be combined!'); | ||
throw new UsageError('cannot combine --raw and --path!'); | ||
} | ||
@@ -177,16 +216,34 @@ if (args.raw && args.file.length) { | ||
if (args.edit && args.path) { | ||
throw new UsageError('options --edit and --path cannot be combined!'); | ||
throw new UsageError('cannot combine --edit and --path!'); | ||
} | ||
if (args.edit && args.raw) { | ||
throw new UsageError('options --edit and --raw cannot be combined!'); | ||
throw new UsageError('cannot combine --edit and --raw!'); | ||
} | ||
if (args.edit && args.keep) { | ||
throw new UsageError('cannot combine --edit and --keep!'); | ||
} | ||
if (args.edit && args.encrypt) { | ||
throw new UsageError('cannot combine --edit and --encrypt!'); | ||
} | ||
if (args.edit && args.decrypt) { | ||
throw new UsageError('cannot combine --edit and --decrypt!'); | ||
} | ||
if (args.edit && !args.file.length) { | ||
throw new UsageError('option --edit used, but no files given!'); | ||
} | ||
if (!args.key && !args.key_fd && (!config.keys || !config.keys.length)) { | ||
if (!args.generate_key && !args.k && (!config.keys || !config.keys.length)) { | ||
throw new UsageError('no keys given and no default keys configured!'); | ||
} | ||
if (args.rm && !args.file.length) { | ||
throw new UsageError('option --rm used, but no files given!'); | ||
if (args.keep && !args.file.length) { | ||
throw new UsageError('option --keep used, but no files given!'); | ||
} | ||
if (args.generate_key && args.encrypt) { | ||
throw new UsageError('cannot combine --generate-key and --encrypt!'); | ||
} | ||
if (args.generate_key && args.decrypt) { | ||
throw new UsageError('cannot combine --generate-key and --decrypt!'); | ||
} | ||
if (args.generate_key && args.file && args.file.length) { | ||
throw new UsageError('option --generate-key used, but files given!'); | ||
} | ||
try { | ||
@@ -214,45 +271,42 @@ _run(args, config, options); | ||
} | ||
const keys = []; | ||
if (args.key || args.key_fd) { | ||
if (args.key) { | ||
keys.push(...args.key.map(key => readKey(key))); | ||
} | ||
if (args.key_fd) { | ||
for (const key of args.key_fd) { | ||
const fd = parseInt(key); | ||
if (fd || fd === 0) { | ||
const str = readFd(fd); | ||
keys.push(str.trim()); | ||
} else { | ||
throw new UsageError(`not a file descriptor: ${key}`); | ||
} | ||
} | ||
} | ||
} else if (config.keys) { | ||
for (const obj of config.keys) { | ||
if (obj.file && obj.key) { | ||
throw new ConfigurationError('either file or key must be set, not both!'); | ||
} else if (obj.file) { | ||
keys.push(readKey(obj.file)); | ||
} else if (obj.key) { | ||
const type = typeof (obj.key); | ||
if (type === 'string') { | ||
keys.push(obj.key.trim()); | ||
} else if (Buffer.isBuffer(obj.key)) { | ||
keys.push(obj.key.toString('utf8').trim()); | ||
} else { | ||
throw new ConfigurationError(`key entry is not a string: ${type}`); | ||
} | ||
let input; | ||
if (options.stdin) { | ||
input = options.stdin; | ||
} else { | ||
input = process.stdin; | ||
} | ||
let output; | ||
if (options.stdout) { | ||
output = options.stdout; | ||
} else { | ||
output = process.stdout; | ||
output.on('error', err => { | ||
if (err && err.code === 'EPIPE') { | ||
console.error('broken pipe'); | ||
} else { | ||
throw new ConfigurationError('neither file nor key given for key entry!'); | ||
console.error('unknown I/O error!'); | ||
} | ||
} | ||
}); | ||
} | ||
if (args.edit) { | ||
const configKeys = readConfigKeys(config); | ||
const keys = []; | ||
if (args.k) { | ||
keys.push(...args.k.map(k => readKey(configKeys, k))); | ||
} else { | ||
configKeys.forEach(k => keys.push(k.key)); | ||
} | ||
const encryptionKey = (args.K | ||
? readKey(configKeys, args.K) | ||
: (keys.length === 1 ? keys[0] : null)); | ||
if (args.generate_key) { | ||
const key = yamlcrypt.generateKey(algorithm); | ||
output.write(key); | ||
output.write('\n'); | ||
} else if (args.edit) { | ||
for (const file of args.file) { | ||
editFile(file, keys, algorithm, args, config); | ||
editFile(file, keys, encryptionKey, algorithm, args, config); | ||
} | ||
} else if (args.file.length) { | ||
for (const file of args.file) { | ||
processFileArg(file, keys, algorithm, args); | ||
processFileArg(file, keys, encryptionKey, algorithm, args); | ||
} | ||
@@ -268,51 +322,154 @@ } else { | ||
} | ||
if (encrypt && keys.length > 1) { | ||
throw new UsageError(`encrypting, but more than one key given!`); | ||
if (encrypt) { | ||
checkEncryptionKey(keys, encryptionKey); | ||
} | ||
let input; | ||
if (options.stdin) { | ||
input = options.stdin; | ||
} else { | ||
input = readFd(process.stdin.fd); | ||
} | ||
let output; | ||
if (options.stdout) { | ||
output = options.stdout; | ||
} else { | ||
output = process.stdout; | ||
} | ||
const opts = { 'base64': args.base64, 'algorithm': algorithm }; | ||
if (args.raw) { | ||
if (encrypt) { | ||
const crypt = yamlcrypt.encrypt(keys[0], opts); | ||
output.write(crypt.encryptRaw(input)); | ||
output.write('\n'); | ||
readInput(input, buf => { | ||
if (args.raw) { | ||
if (encrypt) { | ||
const crypt = yamlcrypt.encrypt(encryptionKey, opts); | ||
output.write(crypt.encryptRaw(buf)); | ||
output.write('\n'); | ||
} else { | ||
const result = tryDecrypt(opts, keys, crypt => crypt.decryptRaw(buf)); | ||
output.write(result); | ||
} | ||
} else { | ||
const crypt = yamlcrypt.decrypt(keys[0], opts); | ||
let result = crypt.decryptRaw(input); | ||
output.write(result); | ||
let strs = []; | ||
if (encrypt) { | ||
const crypt = yamlcrypt.encrypt(encryptionKey, opts); | ||
yaml.safeLoadAll(buf, obj => { | ||
yamlcryptHelper.processStrings(obj, args.path, str => new yamlcrypt.Plaintext(str)); | ||
const encrypted = crypt.safeDump(obj); | ||
strs.push(encrypted); | ||
}); | ||
} else { | ||
strs = tryDecrypt(opts, keys, crypt => { | ||
const result = []; | ||
crypt.safeLoadAll(buf, obj => result.push(yaml.safeDump(obj))); | ||
return result; | ||
}); | ||
} | ||
for (let idx = 0; idx < strs.length; idx++) { | ||
if (idx > 0) { | ||
output.write('---\n'); | ||
} | ||
output.write(strs[idx]); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
function readInput(input, callback) { | ||
if (typeof input === 'string' || input instanceof String || Buffer.isBuffer(input)) { | ||
callback(input); | ||
} else { | ||
const ret = []; | ||
let len = 0; | ||
input.on('readable', () => { | ||
let chunk; | ||
while ((chunk = input.read())) { | ||
ret.push(chunk); | ||
len += chunk.length; | ||
} | ||
}); | ||
input.on('end', () => { | ||
callback(Buffer.concat(ret, len)); | ||
}); | ||
} | ||
} | ||
function checkEncryptionKey(keys, encryptionKey) { | ||
if (!encryptionKey) { | ||
if (keys.length) { | ||
throw new UsageError('encrypting, but multiple keys given! ' | ||
+ 'Use -K to explicitly specify an encryption key.'); | ||
} else { | ||
const strs = []; | ||
if (encrypt) { | ||
const crypt = yamlcrypt.encrypt(keys[0], opts); | ||
yaml.safeLoadAll(input, obj => { | ||
yamlcryptHelper.processStrings(obj, args.path, str => new yamlcrypt.Plaintext(str)); | ||
const encrypted = crypt.safeDump(obj); | ||
strs.push(encrypted); | ||
}); | ||
throw new UsageError('encrypting, but no keys given!'); | ||
} | ||
} | ||
} | ||
function readConfigKeys(config) { | ||
const keys = []; | ||
if (Array.isArray(config.keys)) { | ||
for (const obj of config.keys) { | ||
if (obj.key) { | ||
let key; | ||
const type = typeof (obj.key); | ||
if (type === 'string') { | ||
key = obj.key.trim(); | ||
} else if (Buffer.isBuffer(obj.key)) { | ||
key = obj.key.toString('utf8').trim(); | ||
} else { | ||
throw new ConfigurationError(`key entry is not a string: ${type}`); | ||
} | ||
const name = obj.name || ''; | ||
keys.push({ key, name }); | ||
} else { | ||
const crypt = yamlcrypt.decrypt(keys[0], opts); | ||
crypt.safeLoadAll(input, obj => strs.push(yaml.safeDump(obj))); | ||
throw new ConfigurationError('attribute key missing for key entry!'); | ||
} | ||
for (let idx = 0; idx < strs.length; idx++) { | ||
if (idx > 0) { | ||
output.write('---\n'); | ||
} | ||
} | ||
for (let i = 0; i < keys.length; i++) { | ||
for (let j = 0; j < keys.length; j++) { | ||
if (i !== j) { | ||
if (keys[i].name && keys[i].name === keys[j].name) { | ||
throw new ConfigurationError(`non-unique key name: ${keys[i].name}`); | ||
} | ||
output.write(strs[idx]); | ||
} | ||
} | ||
} | ||
return keys; | ||
} | ||
function readKey(configKeys, key) { | ||
let prefix; | ||
let arg; | ||
if (key.includes(':')) { | ||
const idx = key.indexOf(':'); | ||
prefix = key.substring(0, idx); | ||
arg = key.substring(idx + 1); | ||
} else { | ||
prefix = 'f'; | ||
arg = key; | ||
} | ||
if (prefix === 'c' || prefix === 'config') { | ||
for (const k of configKeys) { | ||
if (k.name === arg) { | ||
return k.key; | ||
} | ||
} | ||
throw new UsageError(`key not found in configuration file: ${arg}`); | ||
} else if (prefix === 'e' || prefix === 'env') { | ||
const str = process.env[arg]; | ||
if (!str || !str.trim()) { | ||
throw new UsageError(`no such environment variable: ${arg}`); | ||
} | ||
return str.trim(); | ||
} else if (prefix === 'fd') { | ||
const fd = parseInt(arg); | ||
if (fd || fd === 0) { | ||
return readFd(fd).trim(); | ||
} else { | ||
throw new UsageError(`not a file descriptor: ${arg}`); | ||
} | ||
} else if (prefix === 'f' || prefix === 'file') { | ||
let raw; | ||
try { | ||
raw = fs.readFileSync(arg); | ||
} catch (e) { | ||
if (e.code === 'ENOENT') { | ||
throw new UsageError(`key file does not exist: ${arg}`); | ||
} else { | ||
throw e; | ||
} | ||
} | ||
return raw.toString('utf8').trim(); | ||
} else { | ||
throw new UsageError(`unknown key argument: ${key}`); | ||
} | ||
} | ||
function readFd(fd) { | ||
@@ -331,16 +488,2 @@ var buf = Buffer.alloc(1024); | ||
function readKey(keyFile) { | ||
let raw; | ||
try { | ||
raw = fs.readFileSync(keyFile); | ||
} catch (e) { | ||
if (e.code === 'ENOENT') { | ||
throw new UsageError(`key file does not exist: ${keyFile}`); | ||
} else { | ||
throw e; | ||
} | ||
} | ||
return raw.toString('utf8').trim(); | ||
} | ||
function plaintextFile(file) { | ||
@@ -354,3 +497,3 @@ return file.endsWith('.yaml') || file.endsWith('.yml'); | ||
function processFileArg(file, keys, algorithm, args) { | ||
function processFileArg(file, keys, encryptionKey, algorithm, args) { | ||
const stat = fs.statSync(file); | ||
@@ -369,3 +512,3 @@ if (stat.isDirectory()) { | ||
}) | ||
.forEach(f => processFile(file + '/' + f, keys, algorithm, args)); | ||
.forEach(f => processFile(file + '/' + f, keys, encryptionKey, algorithm, args)); | ||
} else { | ||
@@ -375,7 +518,7 @@ throw new UsageError(`directories will be skipped unless --dir given: ${file}`); | ||
} else { | ||
processFile(file, keys, algorithm, args); | ||
processFile(file, keys, encryptionKey, algorithm, args); | ||
} | ||
} | ||
function processFile(file, keys, algorithm, args) { | ||
function processFile(file, keys, encryptionKey, algorithm, args) { | ||
let encrypt; | ||
@@ -394,4 +537,4 @@ if (plaintextFile(file)) { | ||
} | ||
if (encrypt && keys.length != 1) { | ||
throw new UsageError(`encrypting file, but more than one key given!`); | ||
if (encrypt) { | ||
checkEncryptionKey(keys, encryptionKey); | ||
} | ||
@@ -415,3 +558,3 @@ let content; | ||
if (encrypt) { | ||
const crypt = yamlcrypt.encrypt(keys[0], opts); | ||
const crypt = yamlcrypt.encrypt(encryptionKey, opts); | ||
yaml.safeLoadAll(content, obj => { | ||
@@ -423,25 +566,35 @@ yamlcryptHelper.processStrings(obj, args.path, str => new yamlcrypt.Plaintext(str)); | ||
} else { | ||
let success = false; | ||
for (const key of keys) { | ||
try { | ||
strs = []; | ||
const crypt = yamlcrypt.decrypt(key, opts); | ||
crypt.safeLoadAll(content, obj => strs.push(yaml.safeDump(obj))); | ||
success = true; | ||
break; | ||
} catch (e) { | ||
continue; | ||
} | ||
} | ||
if (!success) { | ||
throw new Error('No matching key to decrypt the given data!'); | ||
} | ||
strs = tryDecrypt(opts, keys, crypt => { | ||
const result = []; | ||
crypt.safeLoadAll(content, obj => result.push(yaml.safeDump(obj))); | ||
return result; | ||
}); | ||
} | ||
if (!args.keep) { | ||
fs.renameSync(file, output); | ||
} | ||
writeYaml(strs, output); | ||
if (args.rm) { | ||
fs.unlinkSync(file); | ||
} | ||
function tryDecrypt(opts, keys, callback) { | ||
let result = null; | ||
let success = false; | ||
for (const key of keys) { | ||
try { | ||
const crypt = yamlcrypt.decrypt(key, opts); | ||
result = callback(crypt); | ||
success = true; | ||
break; | ||
} catch (e) { | ||
continue; | ||
} | ||
} | ||
if (success) { | ||
return result; | ||
} else { | ||
throw new Error('no matching key to decrypt the given data!'); | ||
} | ||
} | ||
function editFile(file, keys, algorithm, args, config) { | ||
function editFile(file, keys, encryptionKey, algorithm, args, config) { | ||
let content; | ||
@@ -462,18 +615,24 @@ try { | ||
const opts = { 'base64': args.base64, 'algorithm': algorithm }; | ||
const transformed = yamlcryptHelper.transform(content, keys, opts, str => { | ||
const tmpFile = tmp.fileSync({ 'dir': dir, 'postfix': '.yaml' }); | ||
fs.writeSync(tmpFile.fd, str); | ||
fs.closeSync(tmpFile.fd); | ||
const tmpFile = tmp.fileSync({ 'dir': dir, 'postfix': '.yaml', 'keep': true }); | ||
try { | ||
const opts = { 'base64': args.base64, 'algorithm': algorithm }; | ||
const transformed = yamlcryptHelper.transform(content, keys, encryptionKey, opts, str => { | ||
fs.writeSync(tmpFile.fd, str); | ||
fs.closeSync(tmpFile.fd); | ||
childProcess.spawnSync(editor, [tmpFile.name], { 'stdio': 'inherit' }); | ||
childProcess.spawnSync(editor, [tmpFile.name], { 'stdio': 'inherit' }); | ||
return fs.readFileSync(tmpFile.name); | ||
}); | ||
fs.writeFileSync(file, transformed); | ||
return fs.readFileSync(tmpFile.name); | ||
}); | ||
fs.writeFileSync(tmpFile.name, transformed); | ||
fs.renameSync(tmpFile.name, file); | ||
} finally { | ||
if (fs.existsSync(tmpFile.name)) { | ||
fs.unlinkSync(tmpFile.name); | ||
} | ||
} | ||
} | ||
function writeYaml(strs, file) { | ||
const fd = fs.openSync(file, 'wx'); | ||
const fd = fs.openSync(file, 'w'); | ||
try { | ||
@@ -480,0 +639,0 @@ for (let idx = 0; idx < strs.length; idx++) { |
@@ -0,1 +1,3 @@ | ||
const crypto = require('crypto'); | ||
const URLSafeBase64 = require('urlsafe-base64'); | ||
@@ -11,2 +13,10 @@ const fernet = require('fernet'); | ||
/** | ||
* Generate a new key that can be used for Fernet cryptography. | ||
* @returns {string} Randomly generated key | ||
*/ | ||
function fernetGenerateKey() { | ||
return generateKey(32); | ||
} | ||
/** | ||
* Encrypts the message with the given key. | ||
@@ -41,2 +51,10 @@ * @param {string} key Key to use for encryption, must be exactly 32 bytes when encoded in UTF-8 | ||
/** | ||
* Generates a new key that can be used for Branca cryptography. | ||
* @returns {string} Randomly generated key | ||
*/ | ||
function brancaGenerateKey() { | ||
return generateKey(32); | ||
} | ||
/** | ||
* Encrypts the message with the given key. | ||
@@ -62,2 +80,8 @@ * @param {string} key Key to use for encryption, must be exactly 32 bytes when encoded in UTF-8 | ||
function generateKey(length) { | ||
const buf = crypto.randomBytes(length); | ||
return buf.toString('base64').substring(0, length); | ||
} | ||
module.exports.fernetGenerateKey = fernetGenerateKey; | ||
module.exports.fernetEncrypt = fernetEncrypt; | ||
@@ -67,3 +91,4 @@ module.exports.fernetDecrypt = fernetDecrypt; | ||
module.exports.brancaDefaults = brancaDefaults; | ||
module.exports.brancaGenerateKey = brancaGenerateKey; | ||
module.exports.brancaEncrypt = brancaEncrypt; | ||
module.exports.brancaDecrypt = brancaDecrypt; |
@@ -46,17 +46,17 @@ const yaml = require('js-yaml'); | ||
function transform(content, keys, opts, callback) { | ||
function transform(content, keys, encryptionKey, opts, callback) { | ||
let key = null; | ||
const objs = []; | ||
let objs = []; | ||
for (const k of keys) { | ||
const tmp = []; | ||
try { | ||
const tmp = []; | ||
const opts_ = Object.assign({ 'objects': true }, opts); | ||
const crypt = yamlcrypt.decrypt(k, opts_); | ||
crypt.safeLoadAll(content, obj => tmp.push(obj)); | ||
tmp.forEach(obj => objs.push(obj)); | ||
key = k; | ||
break; | ||
} catch (e) { | ||
continue; | ||
} | ||
key = k; | ||
objs = tmp; | ||
break; | ||
} | ||
@@ -68,2 +68,4 @@ | ||
const reencrypt = (key !== encryptionKey); | ||
let index = 0; | ||
@@ -74,3 +76,3 @@ const types = []; | ||
const knownText = new _KnownText(t, index++, t.algorithm); | ||
types.push(_knownTextType(key, knownText)); | ||
types.push(_knownTextType(key, knownText, reencrypt)); | ||
return knownText; | ||
@@ -88,3 +90,3 @@ }); | ||
const crypt = yamlcrypt.encrypt(key, opts); | ||
const crypt = yamlcrypt.encrypt(encryptionKey, opts); | ||
return crypt.safeDumpAll(result); | ||
@@ -101,3 +103,3 @@ } | ||
function _knownTextType(key, knownText) { | ||
function _knownTextType(key, knownText, reencrypt) { | ||
return new yaml.Type('!yaml-crypt/:' + knownText.index, { | ||
@@ -109,3 +111,3 @@ kind: 'scalar', | ||
construct: data => { | ||
if (data === knownText.plaintext.plaintext) { | ||
if (!reencrypt && data === knownText.plaintext.plaintext) { | ||
return knownText.plaintext; | ||
@@ -112,0 +114,0 @@ } else { |
@@ -15,2 +15,15 @@ const yaml = require('js-yaml'); | ||
/** | ||
* Generate a new key | ||
*/ | ||
function generateKey(algorithm) { | ||
if (!algorithm || algorithm === ALGORITHM_FERNET) { | ||
return crypto.fernetGenerateKey(); | ||
} else if (algorithm === ALGORITHM_BRANCA) { | ||
return crypto.brancaGenerateKey(); | ||
} else { | ||
throw new Error(`unsupported algorithm: ${algorithm}`); | ||
} | ||
} | ||
/** | ||
* Plain text, unencrypted | ||
@@ -163,3 +176,3 @@ */ | ||
const decrypted = decrypt(key, data); | ||
const decoded = (base64 ? new Buffer(decrypted, 'base64').toString() : decrypted); | ||
const decoded = (base64 ? Buffer.from(decrypted, 'base64').toString() : decrypted); | ||
return (objects ? new Plaintext(decoded, data, algorithm) : decoded); | ||
@@ -176,3 +189,3 @@ }, | ||
const str = data.toString(); | ||
const encoded = (base64 ? new Buffer(str).toString('base64') : str); | ||
const encoded = (base64 ? Buffer.from(str).toString('base64') : str); | ||
encrypted = encrypt(key, encoded); | ||
@@ -188,2 +201,3 @@ } | ||
module.exports.algorithms = algorithms; | ||
module.exports.generateKey = generateKey; | ||
module.exports.Plaintext = Plaintext; | ||
@@ -190,0 +204,0 @@ module.exports.Ciphertext = Ciphertext; |
{ | ||
"name": "yaml-crypt", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "Encrypt and decrypt YAML documents", | ||
"license": "MIT", | ||
"author": "Pascal", | ||
"homepage": "https://github.com/pascalgn/yaml-crypt", | ||
"homepage": "https://github.com/autoapply/yaml-crypt", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/pascalgn/yaml-crypt.git" | ||
"url": "https://github.com/autoapply/yaml-crypt.git" | ||
}, | ||
@@ -20,3 +20,3 @@ "main": "./lib/yaml-crypt.js", | ||
"fernet": "^0.3.1", | ||
"js-yaml": "^3.11.0", | ||
"js-yaml": "^3.12.0", | ||
"pkginfo": "^0.4.1", | ||
@@ -34,6 +34,6 @@ "tmp": "^0.0.33", | ||
"coveralls": "^3.0.0", | ||
"eslint": "^4.10.0", | ||
"eslint": "^5.5.0", | ||
"mocha": "^5.1.0", | ||
"nyc": "^11.6.0" | ||
"nyc": "^13.0.1" | ||
} | ||
} |
# yaml-crypt | ||
[![Build Status](https://img.shields.io/travis/pascalgn/yaml-crypt.svg?style=flat-square)](https://travis-ci.org/pascalgn/yaml-crypt) | ||
[![Coverage status](https://img.shields.io/coveralls/github/pascalgn/yaml-crypt.svg?style=flat-square)](https://coveralls.io/github/pascalgn/yaml-crypt) | ||
[![Build Status](https://img.shields.io/travis/autoapply/yaml-crypt.svg?style=flat-square)](https://travis-ci.org/autoapply/yaml-crypt) | ||
[![Coverage status](https://img.shields.io/coveralls/github/autoapply/yaml-crypt.svg?style=flat-square)](https://coveralls.io/github/autoapply/yaml-crypt) | ||
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) | ||
@@ -27,12 +27,8 @@ | ||
both [Fernet](https://github.com/fernet/spec/blob/master/Spec.md) | ||
and [Branca](https://branca.io/) encryption schemes are supported, | ||
so you will need a key with exactly 32 bytes. | ||
The easiest way is to use the [pwgen](https://linux.die.net/man/1/pwgen) command: | ||
and [Branca](https://branca.io/) encryption schemes are supported. | ||
$ pwgen 32 1 > my-key | ||
To generate a new random key, run | ||
Another way would be to use the `urandom` device file: | ||
$ yaml-crypt --generate-key > my-key | ||
$ cat /dev/urandom | LC_ALL=C tr -dc A-Za-z0-9 | head -c 32 > my-key | ||
To encrypt all values in a YAML file, run | ||
@@ -42,8 +38,4 @@ | ||
This will generate the file `my-file.yaml-crypt`, while leaving `my-file.yaml` intact. | ||
If you want to delete the original file after encryption, use the `--rm` option. | ||
This will generate the file `my-file.yaml-crypt`. | ||
> Files will be deleted using [unlink](https://linux.die.net/man/2/unlink). | ||
> If this does not meet your security needs, consider removing the file manually instead! | ||
The operation will be performed based on the file extension, so to decrypt a file, | ||
@@ -74,8 +66,7 @@ just use | ||
The yaml-crypt command looks in `~/.yaml-crypt` for a file `config.yaml` or `config.yml`. | ||
Currently, only the `keys` property is supported. These keys will be used when no key | ||
files are given on the command line: | ||
Currently, only the `keys` property is supported. These keys will be used when no keys | ||
are given on the command line: | ||
$ cat ~/.yaml-crypt/config.yaml | ||
keys: | ||
- file: /home/user/.my-key-file | ||
- key: my-raw-key-data | ||
@@ -82,0 +73,0 @@ - key: !!binary my-base64-key-data |
@@ -43,3 +43,3 @@ const fs = require('fs'); | ||
expect(() => runWithKeyFile(['--path', 'x', '--raw'], {}, { 'stdout': new Out() })) | ||
.to.throw(/cannot be combined/); | ||
.to.throw(/cannot combine/); | ||
}); | ||
@@ -61,5 +61,24 @@ | ||
expect(() => runWithKeyFile(['-k', secondKeyFile.name, '-e'], {}, { 'stdout': new Out() })) | ||
.to.throw(/more than one key/); | ||
.to.throw(/encrypting, but multiple keys given/); | ||
}); | ||
it('should throw an error when trying to read a nonexisting environment variable', () => { | ||
expect(() => yamlcryptcli.run(['-k', 'env:YAML_CRYPT_321'], {}, { 'stdout': new Out() })) | ||
.to.throw(/no such environment variable/); | ||
}); | ||
it('should throw an error when passing an invalid file descriptor', () => { | ||
expect(() => yamlcryptcli.run(['-k', 'fd:x'], {}, { 'stdout': new Out() })) | ||
.to.throw(/not a file descriptor/); | ||
}); | ||
it('should generate a key', () => { | ||
const options = { | ||
'stdin': '', | ||
'stdout': new Out() | ||
}; | ||
yamlcryptcli.run(['--debug', '--generate-key'], {}, options); | ||
expect(options.stdout.str.trimRight()).to.have.lengthOf(32); | ||
}); | ||
it('should encrypt the given YAML file (fernet)', () => { | ||
@@ -101,13 +120,20 @@ const input = tmp.fileSync({ 'postfix': '.yaml' }); | ||
it('should remove the old files when using --rm', () => { | ||
it('should remove the old files', () => { | ||
const input = tmp.fileSync({ 'postfix': '.yaml' }); | ||
fs.copyFileSync('./test/test-2.yaml', input.name); | ||
runWithKeyFile(['--rm', input.name], {}, { 'stdout': new Out() }); | ||
runWithKeyFile([input.name], {}, { 'stdout': new Out() }); | ||
expect(fs.existsSync(input.name)).to.equal(false); | ||
}); | ||
it('should not remove the old files when using --keep', () => { | ||
const input = tmp.fileSync({ 'postfix': '.yaml' }); | ||
fs.copyFileSync('./test/test-2.yaml', input.name); | ||
runWithKeyFile(['--keep', input.name], {}, { 'stdout': new Out() }); | ||
expect(fs.existsSync(input.name)).to.equal(true); | ||
}); | ||
function runWithKeyFile(argv, config, options) { | ||
const keyFile = tmp.fileSync(); | ||
fs.writeSync(keyFile.fd, 'aehae5Ui0Eechaeghau9Yoh9jufiep7H'); | ||
return yamlcryptcli.run(['-k', keyFile.name].concat(argv), config, options); | ||
return yamlcryptcli.run(['--debug', '-k', keyFile.name].concat(argv), config, options); | ||
} | ||
@@ -121,8 +147,36 @@ | ||
expect(() => yamlcryptcli.run(['-k', keyFile.name, input.name], {}, { 'stdout': new Out() })) | ||
.to.throw(/No matching key/); | ||
.to.throw(/no matching key/); | ||
}); | ||
it('should throw an error when no named key is available in the config file', () => { | ||
const config = { | ||
'keys': [] | ||
}; | ||
const options = { | ||
'stdin': '', | ||
'stdout': new Out() | ||
}; | ||
expect(() => yamlcryptcli.run(['-k', 'config:name1', '-d'], config, options)) | ||
.to.throw(/key not found in configuration file/); | ||
}); | ||
it('should throw an error when the key names are not unique in the config file', () => { | ||
const config = { | ||
'keys': [ | ||
{ 'key': 'a', 'name': 'key1' }, | ||
{ 'key': 'b', 'name': 'key1' } | ||
] | ||
}; | ||
const options = { | ||
'stdin': '', | ||
'stdout': new Out() | ||
}; | ||
expect(() => yamlcryptcli.run(['-d'], config, options)) | ||
.to.throw(/non-unique key name/); | ||
}); | ||
it('should decrypt the given input', () => { | ||
const config = { | ||
'keys': [ | ||
{ 'key': 'INVALID_KEY____________________X' }, | ||
{ 'key': 'aehae5Ui0Eechaeghau9Yoh9jufiep7H' } | ||
@@ -140,8 +194,24 @@ ] | ||
it('should decrypt the given input when using --raw', () => { | ||
const config = { | ||
'keys': [ | ||
{ 'key': 'INVALID_KEY_123________________X' }, | ||
{ 'key': 'aehae5Ui0Eechaeghau9Yoh9jufiep7H' }, | ||
{ 'key': 'INVALID_KEY_345________________X' } | ||
] | ||
}; | ||
const input = 'gAAAAAAAAAABAAECAwQFBgcICQoLDA0OD7nQ_JQsjDx78n7mQ9bW3T-rgiTN7WX3Uq66EDA0qxZDNQppXL6WaOAIW4x8ElmcRg=='; | ||
const options = { | ||
'stdin': input, | ||
'stdout': new Out() | ||
}; | ||
yamlcryptcli.run(['-d', '--raw'], config, options); | ||
expect(options.stdout.str).to.equal('Hello, world!'); | ||
}); | ||
it('should encrypt the whole input when using --raw', () => { | ||
const keyFile = tmp.fileSync(); | ||
fs.writeSync(keyFile.fd, 'aehae5Ui0Eechaeghau9Yoh9jufiep7H'); | ||
const config = { | ||
'keys': [ | ||
{ 'file': keyFile.name } | ||
{ 'key': 'KEY_THAT_SHOULD_NOT_BE_USED_____', 'name': 'key1' }, | ||
{ 'key': 'aehae5Ui0Eechaeghau9Yoh9jufiep7H', 'name': 'key2' } | ||
] | ||
@@ -153,3 +223,3 @@ }; | ||
}; | ||
yamlcryptcli.run(['-e', '--raw'], config, options); | ||
yamlcryptcli.run(['-e', '--raw', '-K', 'c:key2'], config, options); | ||
const expected = 'gAAAAAAAAAABAAECAwQFBgcICQoLDA0OD7nQ_JQsjDx78n7mQ9bW3T-rgiTN7WX3Uq66EDA0qxZDNQppXL6WaOAIW4x8ElmcRg==\n'; | ||
@@ -166,3 +236,3 @@ expect(options.stdout.str).to.equal(expected); | ||
yamlcryptcli.run(['-k', keyFile.name, '--edit', input.name], { 'editor': 'touch' }, {}); | ||
yamlcryptcli.run(['--debug', '-k', keyFile.name, '--edit', input.name], { 'editor': 'touch' }, {}); | ||
@@ -169,0 +239,0 @@ const output = fs.readFileSync(input.name); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
53943
1289
78
5
Updatedjs-yaml@^3.12.0