yaml-crypt
Advanced tools
Comparing version 0.3.4 to 0.4.0
@@ -15,5 +15,5 @@ { | ||
"no-tabs": "error", | ||
"no-console": "off", | ||
"no-console": "warn", | ||
"no-constant-condition": "off" | ||
} | ||
} |
#!/usr/bin/env node | ||
const os = require('os'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const process = require('process'); | ||
const childProcess = require('child_process'); | ||
/* eslint-disable no-console */ | ||
const tmp = require('tmp'); | ||
const argparse = require('argparse'); | ||
const yaml = require('js-yaml'); | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
const process = require("process"); | ||
const childProcess = require("child_process"); | ||
const yamlcrypt = require('../lib/yaml-crypt'); | ||
const yamlcryptHelper = require('../lib/yaml-crypt-helper'); | ||
const tmp = require("tmp"); | ||
const argparse = require("argparse"); | ||
require('pkginfo')(module); | ||
const { | ||
algorithms, | ||
loadConfig, | ||
generateKey, | ||
yamlcrypt, | ||
encrypt, | ||
decrypt | ||
} = require("../lib/yaml-crypt"); | ||
const { UsageError, safeDumpAll, tryDecrypt } = require("../lib/utils"); | ||
require("pkginfo")(module); | ||
function main() { | ||
let cfg; | ||
try { | ||
cfg = config(); | ||
} catch (e) { | ||
console.warn('could not read config file, using default!'); | ||
if (e.message) { | ||
console.warn(`error: ${e.message}`); | ||
} | ||
cfg = {}; | ||
let cfg; | ||
try { | ||
cfg = loadConfig(); | ||
} catch (e) { | ||
console.warn("could not read config file, using default!"); | ||
if (e.message) { | ||
console.warn(`error: ${e.message}`); | ||
} | ||
try { | ||
run(null, cfg, {}); | ||
} catch (e) { | ||
handleError(e); | ||
} | ||
cfg = {}; | ||
} | ||
try { | ||
run(null, cfg, {}); | ||
} catch (e) { | ||
handleError(e); | ||
} | ||
} | ||
function handleError(e) { | ||
if (e instanceof ExitError) { | ||
process.exit(e.status); | ||
} else if (e instanceof UsageError || e instanceof UnknownError) { | ||
console.error(`${module.exports.name}: error: ${e.message}`); | ||
process.exit(5); | ||
} else if (e instanceof ConfigurationError) { | ||
console.error(`${module.exports.name}: could not parse configuration: ${e.message}`); | ||
process.exit(6); | ||
} else { | ||
throw e; | ||
} | ||
if (e instanceof ExitError) { | ||
process.exit(e.status); | ||
} else if (e instanceof UsageError || e instanceof UnknownError) { | ||
console.error(`${module.exports.name}: error: ${e.message}`); | ||
process.exit(5); | ||
} else if (e instanceof ConfigurationError) { | ||
console.error( | ||
`${module.exports.name}: could not parse configuration: ${e.message}` | ||
); | ||
process.exit(6); | ||
} else { | ||
throw e; | ||
} | ||
} | ||
function config() { | ||
const home = `${os.homedir()}/.yaml-crypt`; | ||
let raw = null; | ||
for (const filename of ['config.yaml', 'config.yml']) { | ||
try { | ||
raw = fs.readFileSync(`${home}/${filename}`); | ||
break; | ||
} catch (e) { | ||
if (e.code === 'ENOENT') { | ||
continue; | ||
} else { | ||
throw e; | ||
} | ||
function run(argv, config = {}, options = {}) { | ||
class Parser extends argparse.ArgumentParser { | ||
exit(status, message) { | ||
if (message) { | ||
if (status === 0) { | ||
this._printMessage(message); | ||
} else { | ||
this._printMessage(message, process.stderr); | ||
} | ||
} | ||
throw new ExitError(status || 0); | ||
} | ||
if (raw) { | ||
return yaml.safeLoad(raw); | ||
} else { | ||
// default config | ||
return {}; | ||
error(err) { | ||
if (err instanceof ExitError) { | ||
throw err; | ||
} else { | ||
super.error(err); | ||
} | ||
} | ||
} | ||
function run(argv, config = {}, options = {}) { | ||
class Parser extends argparse.ArgumentParser { | ||
exit(status, message) { | ||
if (message) { | ||
if (status === 0) { | ||
this._printMessage(message); | ||
} else { | ||
this._printMessage(message, process.stderr); | ||
} | ||
} | ||
throw new ExitError(status || 0); | ||
_printMessage(message, stream) { | ||
if (message) { | ||
if (options.stdout) { | ||
stream = options.stdout; | ||
} else if (!stream) { | ||
stream = process.stdout; | ||
} | ||
error(err) { | ||
if (err instanceof ExitError) { | ||
throw err; | ||
} else { | ||
super.error(err); | ||
} | ||
} | ||
_printMessage(message, stream) { | ||
if (message) { | ||
if (options.stdout) { | ||
stream = options.stdout; | ||
} else if (!stream) { | ||
stream = process.stdout; | ||
} | ||
stream.write('' + message); | ||
} | ||
} | ||
stream.write("" + message); | ||
} | ||
} | ||
const parser = new Parser({ | ||
prog: module.exports.name, | ||
version: module.exports.version, | ||
addHelp: true, | ||
description: module.exports.description | ||
} | ||
const parser = new Parser({ | ||
prog: module.exports.name, | ||
version: module.exports.version, | ||
addHelp: true, | ||
description: module.exports.description | ||
}); | ||
parser.addArgument(["--debug"], { | ||
action: "storeTrue", | ||
help: "Show debugging output" | ||
}); | ||
parser.addArgument(["-e", "--encrypt"], { | ||
action: "storeTrue", | ||
help: "Encrypt data" | ||
}); | ||
parser.addArgument(["-d", "--decrypt"], { | ||
action: "storeTrue", | ||
help: "Decrypt data" | ||
}); | ||
parser.addArgument(["--generate-key"], { | ||
action: "storeTrue", | ||
help: "Generate a new random key. Use -a to specify the algorithm" | ||
}); | ||
parser.addArgument(["-k"], { | ||
action: "append", | ||
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"], { | ||
metavar: "<algorithm>", | ||
help: | ||
'The encryption algorithm to use. Must be one of "fernet" (default) or "branca"' | ||
}); | ||
parser.addArgument(["-E", "--edit"], { | ||
action: "storeTrue", | ||
help: | ||
"Open an editor for the given files, transparently decrypting and encrypting the file content" | ||
}); | ||
parser.addArgument(["-B", "--base64"], { | ||
action: "storeTrue", | ||
help: | ||
"Encode values using Base64 encoding before encrypting and decode values after decrypting" | ||
}); | ||
parser.addArgument(["--path"], { | ||
metavar: "<yaml-path>", | ||
help: | ||
'Only process values below the given YAML path. For the document {obj:{key:secret},other:[value1,value2]} use "--path=obj.key" to only process "secret"' | ||
}); | ||
parser.addArgument(["--raw"], { | ||
action: "storeTrue", | ||
help: "Encrypt/decrypt raw messages instead of YAML documents" | ||
}); | ||
parser.addArgument(["-D", "--dir"], { | ||
action: "storeTrue", | ||
help: | ||
"Allows to pass directories as input, will process all files within the given directories (non-recursive)" | ||
}); | ||
parser.addArgument(["--keep"], { | ||
action: "storeTrue", | ||
help: "Keep the original files after encryption/decryption" | ||
}); | ||
parser.addArgument(["file"], { | ||
nargs: "*", | ||
metavar: "<file>", | ||
help: "Input file(s) to process" | ||
}); | ||
if ( | ||
(argv && argv.includes("--help")) || | ||
(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.addArgument(['--debug'], { | ||
action: 'storeTrue', | ||
help: 'Show debugging output' | ||
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.addArgument(['-e', '--encrypt'], { | ||
action: 'storeTrue', | ||
help: 'Encrypt data' | ||
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.addArgument(['-d', '--decrypt'], { | ||
action: 'storeTrue', | ||
help: 'Decrypt data' | ||
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".' | ||
}); | ||
parser.addArgument(['--generate-key'], { | ||
action: 'storeTrue', | ||
help: 'Generate a new random key. Use -a to specify the algorithm' | ||
}); | ||
parser.addArgument(['-k'], { | ||
action: 'append', | ||
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'], { | ||
metavar: '<algorithm>', | ||
help: 'The encryption algorithm to use. Must be one of "fernet" (default) or "branca"' | ||
}); | ||
parser.addArgument(['-E', '--edit'], { | ||
action: 'storeTrue', | ||
help: 'Open an editor for the given files, transparently decrypting and encrypting the file content' | ||
}); | ||
parser.addArgument(['-B', '--base64'], { | ||
action: 'storeTrue', | ||
help: 'Encode values using Base64 encoding before encrypting and decode values after decrypting' | ||
}); | ||
parser.addArgument(['--path'], { | ||
metavar: '<yaml-path>', | ||
help: 'Only process values below the given YAML path. For the document {obj:{key:secret},other:[value1,value2]} use "--path=obj.key" to only process "secret"' | ||
}); | ||
parser.addArgument(['--raw'], { | ||
action: 'storeTrue', | ||
help: 'Encrypt/decrypt raw messages instead of YAML documents' | ||
}); | ||
parser.addArgument(['-D', '--dir'], { | ||
action: 'storeTrue', | ||
help: 'Allows to pass directories as input, will process all files within the given directories (non-recursive)' | ||
}); | ||
parser.addArgument(['--keep'], { | ||
action: 'storeTrue', | ||
help: 'Keep the original files after encryption/decryption' | ||
}); | ||
parser.addArgument(['file'], { | ||
nargs: '*', | ||
metavar: '<file>', | ||
help: 'Input file(s) to process' | ||
}); | ||
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".' | ||
}); | ||
parser.epilog = 'For more information, visit https://github.com/autoapply/yaml-crypt'; | ||
parser.epilog = | ||
"For more information, visit https://github.com/autoapply/yaml-crypt"; | ||
} else { | ||
parser.epilog = "For more details, specify --help"; | ||
} | ||
const args = parser.parseArgs(argv); | ||
if (args.encrypt && args.decrypt) { | ||
throw new UsageError("cannot combine --encrypt and --decrypt!"); | ||
} | ||
if (args.raw && args.path) { | ||
throw new UsageError("cannot combine --raw and --path!"); | ||
} | ||
if (args.edit && args.path) { | ||
throw new UsageError("cannot combine --edit and --path!"); | ||
} | ||
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.generate_key && !args.k && (!config.keys || !config.keys.length)) { | ||
throw new UsageError("no keys given and no default keys configured!"); | ||
} | ||
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 { | ||
_run(args, config, options); | ||
} catch (e) { | ||
if (args.debug || e instanceof ConfigurationError) { | ||
throw e; | ||
} else { | ||
parser.epilog = 'For more details, specify --help'; | ||
throw new UnknownError(e.message); | ||
} | ||
const args = parser.parseArgs(argv); | ||
if (args.encrypt && args.decrypt) { | ||
throw new UsageError('cannot combine --encrypt and --decrypt!'); | ||
} | ||
if (args.raw && args.path) { | ||
throw new UsageError('cannot combine --raw and --path!'); | ||
} | ||
if (args.raw && args.file.length) { | ||
throw new UsageError('no files may be given when --raw is used!'); | ||
} | ||
if (args.edit && args.path) { | ||
throw new UsageError('cannot combine --edit and --path!'); | ||
} | ||
if (args.edit && args.raw) { | ||
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.generate_key && !args.k && (!config.keys || !config.keys.length)) { | ||
throw new UsageError('no keys given and no default keys configured!'); | ||
} | ||
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 { | ||
_run(args, config, options); | ||
} catch (e) { | ||
if (args.debug || e instanceof ConfigurationError) { | ||
throw e; | ||
} else { | ||
throw new UnknownError(e.message); | ||
} | ||
} | ||
} | ||
} | ||
function _run(args, config, options) { | ||
let algorithm = null; | ||
for (const a of yamlcrypt.algorithms) { | ||
if (a === args.algorithm || a.startsWith(`${args.algorithm}:`)) { | ||
algorithm = a; | ||
break; | ||
} | ||
let algorithm = null; | ||
for (const a of algorithms) { | ||
if (a === args.algorithm || a.startsWith(`${args.algorithm}:`)) { | ||
algorithm = a; | ||
break; | ||
} | ||
if (args.algorithm && !algorithm) { | ||
throw new UsageError(`unknown encryption algorithm: ${args.algorithm}`); | ||
} | ||
if (args.algorithm && algorithm == null) { | ||
throw new UsageError(`unknown encryption algorithm: ${args.algorithm}`); | ||
} | ||
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 { | ||
console.error("unknown I/O error!"); | ||
} | ||
}); | ||
} | ||
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 = generateKey(algorithm); | ||
output.write(key); | ||
output.write("\n"); | ||
} else if (args.edit) { | ||
for (const file of args.file) { | ||
editFile(file, keys, encryptionKey, algorithm, args, config); | ||
} | ||
let input; | ||
if (options.stdin) { | ||
input = options.stdin; | ||
} else { | ||
input = process.stdin; | ||
} else if (args.file.length) { | ||
for (const file of args.file) { | ||
processFileArg(file, keys, encryptionKey, algorithm, args); | ||
} | ||
let output; | ||
if (options.stdout) { | ||
output = options.stdout; | ||
} else { | ||
let encrypting; | ||
if (args.encrypt) { | ||
encrypting = true; | ||
} else if (args.decrypt) { | ||
encrypting = false; | ||
} else { | ||
output = process.stdout; | ||
output.on('error', err => { | ||
if (err && err.code === 'EPIPE') { | ||
console.error('broken pipe'); | ||
} else { | ||
console.error('unknown I/O error!'); | ||
} | ||
}); | ||
throw new UsageError( | ||
"no input files, but no operation (--encrypt/--decrypt) given!" | ||
); | ||
} | ||
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)); | ||
if (encrypting) { | ||
checkEncryptionKey(keys, encryptionKey); | ||
} | ||
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, encryptionKey, algorithm, args, config); | ||
const opts = { algorithm, base64: args.base64, path: args.path }; | ||
readInput(input, buf => { | ||
if (args.raw) { | ||
if (encrypting) { | ||
const str = args.base64 | ||
? buf.toString("base64") | ||
: buf.toString("utf8"); | ||
const result = encrypt(algorithm, encryptionKey, str); | ||
output.write(result); | ||
output.write("\n"); | ||
} else { | ||
const str = buf.toString("utf8"); | ||
const decrypted = tryDecrypt(algorithms, keys, (algorithm, key) => | ||
decrypt(algorithm, key, str) | ||
); | ||
const result = args.base64 | ||
? Buffer.from(decrypted, "base64").toString("utf8") | ||
: decrypted; | ||
output.write(result); | ||
} | ||
} else if (args.file.length) { | ||
for (const file of args.file) { | ||
processFileArg(file, keys, encryptionKey, algorithm, args); | ||
} | ||
} else { | ||
let encrypt; | ||
if (args.encrypt) { | ||
encrypt = true; | ||
} else if (args.decrypt) { | ||
encrypt = false; | ||
} else { | ||
const str = buf.toString("utf8"); | ||
const crypt = yamlcrypt({ keys, encryptionKey }); | ||
let result; | ||
if (encrypting) { | ||
result = crypt.encryptAll(str, opts); | ||
} else { | ||
throw new UsageError('no input files, but no operation (--encrypt/--decrypt) given!'); | ||
const objs = crypt.decryptAll(str, opts); | ||
result = safeDumpAll(objs); | ||
} | ||
if (encrypt) { | ||
checkEncryptionKey(keys, encryptionKey); | ||
} | ||
const opts = { 'base64': args.base64, 'algorithm': algorithm }; | ||
readInput(input, buf => { | ||
if (args.raw) { | ||
if (encrypt) { | ||
const crypt = yamlcrypt.encrypt(encryptionKey, opts); | ||
output.write(crypt.encryptRaw(buf)); | ||
output.write('\n'); | ||
} else { | ||
const str = buf.toString("utf8"); | ||
const result = tryDecrypt(opts, keys, crypt => crypt.decryptRaw(str)); | ||
output.write(result); | ||
} | ||
} else { | ||
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]); | ||
} | ||
} | ||
}); | ||
} | ||
output.write(result); | ||
} | ||
}); | ||
} | ||
} | ||
function readInput(input, callback) { | ||
if (typeof input === 'string' || input instanceof String || Buffer.isBuffer(input)) { | ||
try { | ||
callback(input); | ||
} catch (e) { | ||
handleError(e); | ||
} | ||
} else { | ||
const ret = []; | ||
let len = 0; | ||
input.on('readable', () => { | ||
let chunk; | ||
while ((chunk = input.read())) { | ||
ret.push(chunk); | ||
len += chunk.length; | ||
} | ||
}); | ||
input.on('end', () => { | ||
try { | ||
callback(Buffer.concat(ret, len)); | ||
} catch (e) { | ||
handleError(e); | ||
} | ||
}); | ||
if ( | ||
typeof input === "string" || | ||
input instanceof String || | ||
Buffer.isBuffer(input) | ||
) { | ||
try { | ||
callback(input); | ||
} catch (e) { | ||
handleError(e); | ||
} | ||
} else { | ||
const ret = []; | ||
let len = 0; | ||
input.on("readable", () => { | ||
let chunk; | ||
while ((chunk = input.read())) { | ||
ret.push(chunk); | ||
len += chunk.length; | ||
} | ||
}); | ||
input.on("end", () => { | ||
try { | ||
callback(Buffer.concat(ret, len)); | ||
} catch (e) { | ||
handleError(e); | ||
} | ||
}); | ||
} | ||
} | ||
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 { | ||
throw new UsageError('encrypting, but no keys given!'); | ||
} | ||
if (!encryptionKey) { | ||
if (keys.length) { | ||
throw new UsageError( | ||
"encrypting, but multiple keys given! " + | ||
"Use -K to explicitly specify an encryption key." | ||
); | ||
} else { | ||
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 { | ||
throw new ConfigurationError('attribute key missing for key entry!'); | ||
} | ||
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 { | ||
throw new ConfigurationError("attribute key missing for key entry!"); | ||
} | ||
} | ||
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}`); | ||
} | ||
} | ||
} | ||
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}`); | ||
} | ||
} | ||
} | ||
return keys; | ||
} | ||
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; | ||
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; | ||
} | ||
} | ||
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(); | ||
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(`unknown key argument: ${key}`); | ||
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) { | ||
var buf = Buffer.alloc(1024); | ||
let str = ''; | ||
while (true) { | ||
var len = fs.readSync(fd, buf, 0, buf.length); | ||
if (!len) { | ||
break; | ||
} | ||
str += buf.toString('utf8', 0, len); | ||
var buf = Buffer.alloc(1024); | ||
let str = ""; | ||
while (true) { | ||
var len = fs.readSync(fd, buf, 0, buf.length); | ||
if (!len) { | ||
break; | ||
} | ||
return str; | ||
str += buf.toString("utf8", 0, len); | ||
} | ||
return str; | ||
} | ||
function plaintextFile(file) { | ||
return file.endsWith('.yaml') || file.endsWith('.yml'); | ||
return file.endsWith(".yaml") || file.endsWith(".yml"); | ||
} | ||
function encryptedFile(file) { | ||
return file.endsWith('.yaml-crypt') || file.endsWith('.yml-crypt'); | ||
return file.endsWith(".yaml-crypt") || file.endsWith(".yml-crypt"); | ||
} | ||
function processFileArg(file, keys, encryptionKey, algorithm, args) { | ||
const stat = fs.statSync(file); | ||
if (stat.isDirectory()) { | ||
if (args.dir) { | ||
fs.readdirSync(file) | ||
.filter(f => { | ||
if (args.encrypt) { | ||
return plaintextFile(f); | ||
} else if (args.decrypt) { | ||
return encryptedFile(f); | ||
} else { | ||
return plaintextFile(f) || encryptedFile(f); | ||
} | ||
}) | ||
.forEach(f => processFile(file + '/' + f, keys, encryptionKey, algorithm, args)); | ||
} else { | ||
throw new UsageError(`directories will be skipped unless --dir given: ${file}`); | ||
} | ||
const stat = fs.statSync(file); | ||
if (stat.isDirectory()) { | ||
if (args.dir) { | ||
fs.readdirSync(file) | ||
.filter(f => { | ||
if (args.encrypt) { | ||
return plaintextFile(f); | ||
} else if (args.decrypt) { | ||
return encryptedFile(f); | ||
} else { | ||
return plaintextFile(f) || encryptedFile(f); | ||
} | ||
}) | ||
.forEach(f => | ||
processFile(file + "/" + f, keys, encryptionKey, algorithm, args) | ||
); | ||
} else { | ||
processFile(file, keys, encryptionKey, algorithm, args); | ||
throw new UsageError( | ||
`directories will be skipped unless --dir given: ${file}` | ||
); | ||
} | ||
} else { | ||
processFile(file, keys, encryptionKey, algorithm, args); | ||
} | ||
} | ||
function processFile(file, keys, encryptionKey, algorithm, args) { | ||
let encrypt; | ||
if (plaintextFile(file)) { | ||
encrypt = true; | ||
} else if (encryptedFile(file)) { | ||
encrypt = false; | ||
let encrypting; | ||
if (plaintextFile(file)) { | ||
encrypting = true; | ||
} else if (encryptedFile(file)) { | ||
encrypting = false; | ||
} else { | ||
throw new UsageError(`unknown file extension: ${file}`); | ||
} | ||
if (encrypting && args.decrypt) { | ||
throw new UsageError(`decrypted file, but --decrypt given: ${file}`); | ||
} else if (!encrypting && args.encrypt) { | ||
throw new UsageError(`encrypted file, but --encrypt given: ${file}`); | ||
} | ||
if (encrypting) { | ||
checkEncryptionKey(keys, encryptionKey); | ||
} | ||
let content; | ||
try { | ||
content = fs.readFileSync(file); | ||
} catch (e) { | ||
if (e.code === "ENOENT") { | ||
throw new UsageError(`file does not exist: ${file}`); | ||
} else { | ||
throw new UsageError(`unknown file extension: ${file}`); | ||
throw e; | ||
} | ||
if (encrypt && args.decrypt) { | ||
throw new UsageError(`decrypted file, but --decrypt given: ${file}`); | ||
} else if (!encrypt && args.encrypt) { | ||
throw new UsageError(`encrypted file, but --encrypt given: ${file}`); | ||
} | ||
if (encrypt) { | ||
checkEncryptionKey(keys, encryptionKey); | ||
} | ||
let content; | ||
try { | ||
content = fs.readFileSync(file); | ||
} catch (e) { | ||
if (e.code === 'ENOENT') { | ||
throw new UsageError(`file does not exist: ${file}`); | ||
} else { | ||
throw e; | ||
} | ||
} | ||
const output = (encrypt ? file + '-crypt' : file.substring(0, file.length - '-crypt'.length)); | ||
if (fs.existsSync(output)) { | ||
throw new UsageError(`output file already exists: ${output}`); | ||
} | ||
let strs = []; | ||
const opts = { 'base64': args.base64, 'algorithm': algorithm }; | ||
if (encrypt) { | ||
const crypt = yamlcrypt.encrypt(encryptionKey, opts); | ||
yaml.safeLoadAll(content, 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(content, obj => result.push(yaml.safeDump(obj))); | ||
return result; | ||
}); | ||
} | ||
if (!args.keep) { | ||
fs.renameSync(file, output); | ||
} | ||
writeYaml(strs, output); | ||
} | ||
} | ||
const output = encrypting | ||
? file + "-crypt" | ||
: file.substring(0, file.length - "-crypt".length); | ||
if (fs.existsSync(output)) { | ||
throw new UsageError(`output file already exists: ${output}`); | ||
} | ||
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 UsageError('no matching key to decrypt the given data!'); | ||
} | ||
const opts = { | ||
algorithm, | ||
base64: args.base64, | ||
path: args.path, | ||
raw: args.raw | ||
}; | ||
const crypt = yamlcrypt({ keys, encryptionKey }); | ||
let result; | ||
if (encrypting) { | ||
result = crypt.encryptAll(content, opts); | ||
} else { | ||
const objs = crypt.decryptAll(content, opts); | ||
result = safeDumpAll(objs); | ||
} | ||
if (!args.keep) { | ||
fs.renameSync(file, output); | ||
} | ||
fs.writeFileSync(output, result); | ||
} | ||
function editFile(file, keys, encryptionKey, algorithm, args, config) { | ||
if (!encryptedFile(file)) { | ||
throw new UsageError(`unexpected extension, expecting .yaml-crypt or .yml-crypt: ${file}`); | ||
} | ||
if (!encryptedFile(file)) { | ||
throw new UsageError( | ||
`unexpected extension, expecting .yaml-crypt or .yml-crypt: ${file}` | ||
); | ||
} | ||
let content; | ||
try { | ||
content = fs.readFileSync(file); | ||
} catch (e) { | ||
if (e.code === 'ENOENT') { | ||
throw new UsageError(`file does not exist: ${file}`); | ||
} else { | ||
throw e; | ||
} | ||
let content; | ||
try { | ||
content = fs.readFileSync(file); | ||
} catch (e) { | ||
if (e.code === "ENOENT") { | ||
throw new UsageError(`file does not exist: ${file}`); | ||
} else { | ||
throw e; | ||
} | ||
} | ||
const dir = path.dirname(path.resolve(file)); | ||
const dir = path.dirname(path.resolve(file)); | ||
const editor = config['editor'] || process.env['EDITOR'] || 'vim'; | ||
const editor = config["editor"] || process.env["EDITOR"] || "vim"; | ||
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); | ||
const tmpFile = tmp.fileSync({ dir: dir, postfix: ".yaml", keep: true }); | ||
try { | ||
const opts = { base64: args.base64, algorithm: algorithm, raw: args.raw }; | ||
const crypt = yamlcrypt({ keys, encryptionKey }); | ||
const transformed = crypt.transform( | ||
content, | ||
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(tmpFile.name, transformed); | ||
fs.renameSync(tmpFile.name, file); | ||
} finally { | ||
if (fs.existsSync(tmpFile.name)) { | ||
fs.unlinkSync(tmpFile.name); | ||
} | ||
return fs.readFileSync(tmpFile.name); | ||
}, | ||
opts | ||
); | ||
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, 'w'); | ||
try { | ||
for (let idx = 0; idx < strs.length; idx++) { | ||
if (idx > 0) { | ||
fs.writeSync(fd, '---\n'); | ||
} | ||
fs.writeSync(fd, strs[idx]); | ||
} | ||
} finally { | ||
fs.closeSync(fd); | ||
} | ||
} | ||
class UnknownError extends Error {} | ||
class UsageError extends Error { } | ||
class ConfigurationError extends Error {} | ||
class UnknownError extends Error { } | ||
class ConfigurationError extends Error { } | ||
class ExitError extends Error { | ||
constructor(status) { | ||
super(`Exit: ${status}`); | ||
this.status = status; | ||
} | ||
constructor(status) { | ||
super(`Exit: ${status}`); | ||
this.status = status; | ||
} | ||
} | ||
@@ -669,3 +648,3 @@ | ||
if (require.main === module) { | ||
main(); | ||
main(); | ||
} |
@@ -1,13 +0,124 @@ | ||
const crypto = require('crypto'); | ||
const crypto = require("crypto"); | ||
const URLSafeBase64 = require('urlsafe-base64'); | ||
const fernet = require('fernet'); | ||
const branca = require('branca'); | ||
const URLSafeBase64 = require("urlsafe-base64"); | ||
const fernet = require("fernet"); | ||
const branca = require("branca"); | ||
const brancaDefaults = { | ||
'ts': undefined, | ||
'nonce': undefined | ||
const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; | ||
const base62 = require("base-x")(BASE62); | ||
const ALGORITHM_FERNET = "fernet:0x80"; | ||
const ALGORITHM_BRANCA = "branca:0xBA"; | ||
const DEFAULT_ALGORITHM = ALGORITHM_FERNET; | ||
const algorithmHandlers = { | ||
[ALGORITHM_FERNET]: { | ||
generateKey: fernetGenerateKey, | ||
encrypt: fernetEncrypt, | ||
decrypt: fernetDecrypt | ||
}, | ||
[ALGORITHM_BRANCA]: { | ||
generateKey: brancaGenerateKey, | ||
encrypt: brancaEncrypt, | ||
decrypt: brancaDecrypt | ||
} | ||
}; | ||
const algorithms = [ALGORITHM_FERNET, ALGORITHM_BRANCA]; | ||
const __brancaDefaults = { | ||
ts: undefined, | ||
nonce: undefined | ||
}; | ||
/** | ||
* Check if the given data is a valid token | ||
* @param {string|Buffer} data Data to check | ||
* @returns {boolean} If the given data is a valid token | ||
*/ | ||
function isToken(data) { | ||
if (typeof data === "string") { | ||
if (data.length > 2) { | ||
if (data[0] === "g" && data[1].match(/[a-zA-Z0-9]/)) { | ||
return Buffer.from(data.substr(0, 2), "base64")[0] === 0x80; | ||
} else { | ||
const str = data.trimRight(); | ||
if (str.match(/^[a-zA-Z0-9]+$/)) { | ||
const result = base62.decodeUnsafe(str); | ||
return result != null && result[0] === 0xba; | ||
} | ||
} | ||
} | ||
} else if (Buffer.isBuffer(data)) { | ||
if (data.length > 2 && data[0] === "g".charCodeAt(0)) { | ||
const buf = Buffer.from(data.slice(0, 2).toString("ascii"), "base64"); | ||
if (buf[0] === 0x80) { | ||
return true; | ||
} | ||
} | ||
try { | ||
const str = data.toString("ascii").trimRight(); | ||
const result = base62.decodeUnsafe(str); | ||
return result != null && result[0] === 0xba; | ||
} catch (e) { | ||
// ignore error! | ||
} | ||
} | ||
return false; | ||
} | ||
/** | ||
* Generate a new random key | ||
* @param {string} algorithm Encryption algorithm | ||
* @returns {string} Randomly generated key | ||
*/ | ||
function generateKey(algorithm) { | ||
return handler(algorithm).generateKey(); | ||
} | ||
/** | ||
* Encrypt the message with the given key. | ||
* @param {string} algorithm Encryption algorithm | ||
* @param {string} key Key to use for encryption | ||
* @param {string} msg String message to encrypt | ||
* @returns {string} Base64-encoded encrypted string | ||
*/ | ||
function encrypt(algorithm, key, msg) { | ||
return handler(algorithm).encrypt(key, msg); | ||
} | ||
/** | ||
* Decrypt the message with the given key. | ||
* @param {string} algorithm Decryption algorithm | ||
* @param {string} key Key to use for decryption | ||
* @param {string} msg Base64 encoded message to decrypt | ||
* @returns {string} Plain text string | ||
*/ | ||
function decrypt(algorithm, key, msg) { | ||
if (msg == null) { | ||
throw new Error("message is null!"); | ||
} else if (typeof msg !== "string") { | ||
throw new Error(`invalid type for message: ${typeof msg}`); | ||
} | ||
return handler(algorithm).decrypt(key, msg); | ||
} | ||
function handler(algorithm) { | ||
if (!algorithm) { | ||
return algorithmHandlers[DEFAULT_ALGORITHM]; | ||
} | ||
let h = algorithmHandlers[algorithm]; | ||
if (h != null) { | ||
return h; | ||
} | ||
for (const a of algorithms) { | ||
if (a.startsWith(`${algorithm}:`)) { | ||
return algorithmHandlers[a]; | ||
} | ||
} | ||
throw new Error(`unknown algorithm: ${algorithm}`); | ||
} | ||
/** | ||
* Generate a new key that can be used for Fernet cryptography. | ||
@@ -17,7 +128,7 @@ * @returns {string} Randomly generated key | ||
function fernetGenerateKey() { | ||
return generateKey(32); | ||
return generateRandomBase64(32); | ||
} | ||
/** | ||
* Encrypts the message with the given key. | ||
* Encrypt the message with the given key. | ||
* @param {string} key Key to use for encryption, must be exactly 32 bytes when encoded in UTF-8 | ||
@@ -28,9 +139,11 @@ * @param {string} msg String message to encrypt | ||
function fernetEncrypt(key, msg) { | ||
const secret = new fernet.Secret(URLSafeBase64.encode(Buffer.from(key, 'utf8'))); | ||
const token = new fernet.Token({ secret: secret }); | ||
return token.encode(msg); | ||
const secret = new fernet.Secret( | ||
URLSafeBase64.encode(Buffer.from(key, "utf8")) | ||
); | ||
const token = new fernet.Token({ secret: secret }); | ||
return token.encode(msg); | ||
} | ||
/** | ||
* Decrypts the message with the given key. | ||
* Decrypt the message with the given key. | ||
* @param {string} key Key to use for decryption, must be exactly 32 bytes when encoded in UTF-8 | ||
@@ -41,22 +154,24 @@ * @param {string} msg Base64 encoded message to decrypt | ||
function fernetDecrypt(key, msg) { | ||
const secret = new fernet.Secret(URLSafeBase64.encode(Buffer.from(key, 'utf8'))); | ||
const token = new fernet.Token({ | ||
secret: secret, | ||
token: msg, | ||
// we currently don't impose any TTL on messages: | ||
ttl: 0 | ||
}); | ||
return token.decode(); | ||
const secret = new fernet.Secret( | ||
URLSafeBase64.encode(Buffer.from(key, "utf8")) | ||
); | ||
const token = new fernet.Token({ | ||
secret: secret, | ||
token: msg, | ||
// we currently don't impose any TTL on messages: | ||
ttl: 0 | ||
}); | ||
return token.decode(); | ||
} | ||
/** | ||
* Generates a new key that can be used for Branca cryptography. | ||
* Generate a new key that can be used for Branca cryptography. | ||
* @returns {string} Randomly generated key | ||
*/ | ||
function brancaGenerateKey() { | ||
return generateKey(32); | ||
return generateRandomBase64(32); | ||
} | ||
/** | ||
* Encrypts the message with the given key. | ||
* Encrypt the message with the given key. | ||
* @param {string} key Key to use for encryption, must be exactly 32 bytes when encoded in UTF-8 | ||
@@ -67,7 +182,7 @@ * @param {string} msg String message to encrypt | ||
function brancaEncrypt(key, msg) { | ||
return branca(key).encode(msg, brancaDefaults.ts, brancaDefaults.nonce); | ||
return branca(key).encode(msg, __brancaDefaults.ts, __brancaDefaults.nonce); | ||
} | ||
/** | ||
* Decrypts the message with the given key. | ||
* Decrypt the message with the given key. | ||
* @param {string} key Key to use for decryption, must be exactly 32 bytes when encoded in UTF-8 | ||
@@ -78,18 +193,18 @@ * @param {string} msg Base62 encoded message to decrypt | ||
function brancaDecrypt(key, msg) { | ||
const payload = branca(key).decode(msg); | ||
return payload.toString(); | ||
const payload = branca(key).decode(msg); | ||
return payload.toString(); | ||
} | ||
function generateKey(length) { | ||
const buf = crypto.randomBytes(length); | ||
return buf.toString('base64').substring(0, length); | ||
function generateRandomBase64(length) { | ||
const buf = crypto.randomBytes(length); | ||
return buf.toString("base64").substring(0, length); | ||
} | ||
module.exports.fernetGenerateKey = fernetGenerateKey; | ||
module.exports.fernetEncrypt = fernetEncrypt; | ||
module.exports.fernetDecrypt = fernetDecrypt; | ||
module.exports.brancaDefaults = brancaDefaults; | ||
module.exports.brancaGenerateKey = brancaGenerateKey; | ||
module.exports.brancaEncrypt = brancaEncrypt; | ||
module.exports.brancaDecrypt = brancaDecrypt; | ||
module.exports = { | ||
__brancaDefaults, | ||
algorithms, | ||
isToken, | ||
generateKey, | ||
encrypt, | ||
decrypt | ||
}; |
@@ -1,141 +0,117 @@ | ||
const yaml = require('js-yaml'); | ||
const yaml = require("js-yaml"); | ||
const yamlcrypt = require('./yaml-crypt'); | ||
const yamlcrypt = require("./yaml-crypt"); | ||
function processStrings(obj, path, callback) { | ||
processValues(obj, path, v => typeof v === 'string', callback); | ||
} | ||
function processValues(obj, path, check, callback) { | ||
let subobj = obj; | ||
if (path) { | ||
const parts = path.split('.'); | ||
for (let idx = 0; idx < parts.length; idx++) { | ||
const part = parts[idx]; | ||
if (idx === parts.length - 1 && check(subobj[part])) { | ||
subobj[part] = callback(subobj[part]); | ||
return; | ||
} else { | ||
subobj = subobj[part]; | ||
} | ||
} | ||
} | ||
for (const key in subobj) { | ||
if (subobj.hasOwnProperty(key)) { | ||
const value = subobj[key]; | ||
if (check(value)) { | ||
subobj[key] = callback(value); | ||
} else if (typeof value === 'object') { | ||
processValues(value, null, check, callback); | ||
} | ||
} | ||
} | ||
} | ||
function safeDumpAll(objs, opts) { | ||
let str = ''; | ||
for (let idx = 0; idx < objs.length; idx++) { | ||
if (idx > 0) { | ||
str += '---\n'; | ||
} | ||
str += yaml.safeDump(objs[idx], opts); | ||
let str = ""; | ||
for (let idx = 0; idx < objs.length; idx++) { | ||
if (idx > 0) { | ||
str += "---\n"; | ||
} | ||
return str; | ||
str += yaml.safeDump(objs[idx], opts); | ||
} | ||
return str; | ||
} | ||
function transform(content, keys, encryptionKey, opts, callback) { | ||
let key = null; | ||
let objs = []; | ||
for (const k of keys) { | ||
const tmp = []; | ||
try { | ||
const opts_ = Object.assign({ 'objects': true }, opts); | ||
const crypt = yamlcrypt.decrypt(k, opts_); | ||
crypt.safeLoadAll(content, obj => tmp.push(obj)); | ||
} catch (e) { | ||
continue; | ||
} | ||
key = k; | ||
objs = tmp; | ||
break; | ||
let key = null; | ||
let objs = []; | ||
for (const k of keys) { | ||
const tmp = []; | ||
try { | ||
const opts_ = Object.assign({ objects: true }, opts); | ||
const crypt = yamlcrypt.decrypt(k, opts_); | ||
crypt.safeLoadAll(content, obj => tmp.push(obj)); | ||
} catch (e) { | ||
continue; | ||
} | ||
key = k; | ||
objs = tmp; | ||
break; | ||
} | ||
if (!key) { | ||
throw new Error('No matching key to decrypt the given data!'); | ||
} | ||
if (!key) { | ||
throw new Error("No matching key to decrypt the given data!"); | ||
} | ||
if (!encryptionKey) { | ||
encryptionKey = key; | ||
} | ||
if (!encryptionKey) { | ||
encryptionKey = key; | ||
} | ||
const reencrypt = (key !== encryptionKey); | ||
const reencrypt = key !== encryptionKey; | ||
let index = 0; | ||
const types = []; | ||
for (const obj of objs) { | ||
processValues(obj, null, v => v instanceof yamlcrypt.Plaintext, t => { | ||
const knownText = new _KnownText(t, index++, t.algorithm); | ||
types.push(_knownTextType(knownText, reencrypt)); | ||
return knownText; | ||
}); | ||
} | ||
function processValues() {} | ||
_newTextTypes().forEach(t => types.push(t)); | ||
let index = 0; | ||
const types = []; | ||
for (const obj of objs) { | ||
processValues( | ||
obj, | ||
null, | ||
v => v instanceof yamlcrypt.Plaintext, | ||
t => { | ||
const knownText = new _KnownText(t, index++, t.algorithm); | ||
types.push(_knownTextType(knownText, reencrypt)); | ||
return knownText; | ||
} | ||
); | ||
} | ||
const schema = yaml.Schema.create(types); | ||
const str = safeDumpAll(objs, { 'schema': schema }); | ||
_newTextTypes().forEach(t => types.push(t)); | ||
const transformed = callback(str); | ||
const schema = yaml.Schema.create(types); | ||
const str = safeDumpAll(objs, { schema: schema }); | ||
const result = []; | ||
yaml.safeLoadAll(transformed, obj => result.push(obj), { 'schema': schema }); | ||
const transformed = callback(str); | ||
const crypt = yamlcrypt.encrypt(encryptionKey, opts); | ||
return crypt.safeDumpAll(result); | ||
const result = []; | ||
yaml.safeLoadAll(transformed, obj => result.push(obj), { schema: schema }); | ||
const crypt = yamlcrypt.encrypt(encryptionKey, opts); | ||
return crypt.safeDumpAll(result); | ||
} | ||
class _KnownText { | ||
constructor(plaintext, index, algorithm) { | ||
this.plaintext = plaintext; | ||
this.index = index; | ||
this.algorithm = algorithm; | ||
} | ||
constructor(plaintext, index, algorithm) { | ||
this.plaintext = plaintext; | ||
this.index = index; | ||
this.algorithm = algorithm; | ||
} | ||
} | ||
function _knownTextType(knownText, reencrypt) { | ||
return new yaml.Type('!yaml-crypt/:' + knownText.index, { | ||
kind: 'scalar', | ||
instanceOf: _KnownText, | ||
predicate: data => data.index === knownText.index, | ||
represent: data => data.plaintext.plaintext, | ||
construct: data => { | ||
if (!reencrypt && data === knownText.plaintext.plaintext) { | ||
return knownText.plaintext; | ||
} else { | ||
return new yamlcrypt.Plaintext(data, null, knownText.algorithm); | ||
} | ||
} | ||
}); | ||
return new yaml.Type("!yaml-crypt/:" + knownText.index, { | ||
kind: "scalar", | ||
instanceOf: _KnownText, | ||
predicate: data => data.index === knownText.index, | ||
represent: data => data.plaintext.plaintext, | ||
construct: data => { | ||
if (!reencrypt && data === knownText.plaintext.plaintext) { | ||
return knownText.plaintext; | ||
} else { | ||
return new yamlcrypt.Plaintext(data, null, knownText.algorithm); | ||
} | ||
} | ||
}); | ||
} | ||
function _newTextTypes() { | ||
const keys = [ | ||
{ 'type': '!yaml-crypt', 'algorithm': yamlcrypt.algorithms[0] } | ||
]; | ||
for (const algorithm of yamlcrypt.algorithms) { | ||
// also allow the usage of just the algorithm name, without version: | ||
const split = algorithm.split(':', 2); | ||
keys.push({ 'type': '!yaml-crypt/' + split[0], 'algorithm': algorithm }); | ||
keys.push({ 'type': '!yaml-crypt/' + algorithm, 'algorithm': algorithm }); | ||
} | ||
return keys.map(key => new yaml.Type(key.type, { | ||
kind: 'scalar', | ||
const keys = [{ type: "!yaml-crypt", algorithm: yamlcrypt.algorithms[0] }]; | ||
for (const algorithm of yamlcrypt.algorithms) { | ||
// also allow the usage of just the algorithm name, without version: | ||
const split = algorithm.split(":", 2); | ||
keys.push({ type: "!yaml-crypt/" + split[0], algorithm: algorithm }); | ||
keys.push({ type: "!yaml-crypt/" + algorithm, algorithm: algorithm }); | ||
} | ||
return keys.map( | ||
key => | ||
new yaml.Type(key.type, { | ||
kind: "scalar", | ||
represent: data => data, | ||
construct: data => new yamlcrypt.Plaintext(data, null, key.algorithm) | ||
})); | ||
}) | ||
); | ||
} | ||
module.exports.processStrings = processStrings; | ||
module.exports.processValues = processValues; | ||
module.exports.safeDumpAll = safeDumpAll; | ||
module.exports.transform = transform; |
@@ -1,202 +0,350 @@ | ||
const yaml = require('js-yaml'); | ||
const { homedir } = require("os"); | ||
const { readFileSync } = require("fs"); | ||
const { join } = require("path"); | ||
const crypto = require('./crypto'); | ||
const yaml = require("js-yaml"); | ||
const ALGORITHM_FERNET = 'fernet:0x80'; | ||
const ALGORITHM_BRANCA = 'branca:0xBA'; | ||
const DEFAULT_ALGORITHM = ALGORITHM_FERNET; | ||
const { | ||
algorithms, | ||
isToken, | ||
generateKey, | ||
encrypt, | ||
decrypt | ||
} = require("./crypto"); | ||
const { | ||
safeDumpAll, | ||
safeLoadAll, | ||
walkStringValues, | ||
walkValues, | ||
tryDecrypt | ||
} = require("./utils"); | ||
/** | ||
* Supported algorithms | ||
*/ | ||
const algorithms = [ALGORITHM_FERNET, ALGORITHM_BRANCA]; | ||
/** | ||
* 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}`); | ||
function loadConfig({ home, path } = {}) { | ||
let content = null; | ||
if (path) { | ||
content = readFileSync(path); | ||
} else { | ||
let h = home ? home : homedir(); | ||
for (const filename of ["config.yaml", "config.yml"]) { | ||
try { | ||
content = readFileSync(join(h, filename)); | ||
break; | ||
} catch (e) { | ||
if (e.code === "ENOENT") { | ||
continue; | ||
} else { | ||
throw e; | ||
} | ||
} | ||
} | ||
} | ||
if (content) { | ||
return yaml.safeLoad(content); | ||
} else { | ||
// default config | ||
return {}; | ||
} | ||
} | ||
/** | ||
* Plain text, unencrypted | ||
*/ | ||
class Plaintext { | ||
constructor(plaintext, ciphertext = null, algorithm = null) { | ||
if (algorithm && !algorithms.includes(algorithm)) { | ||
throw new Error(`unsupported algorithm: ${algorithm}`); | ||
} | ||
this.plaintext = plaintext; | ||
this.ciphertext = ciphertext; | ||
this.algorithm = algorithm; | ||
} | ||
toString() { | ||
return this.plaintext; | ||
} | ||
function yamlcrypt({ keys, encryptionKey } = {}) { | ||
const normalizedKeys = normalizeKeys(keys); | ||
const normalizedEncryptionKey = normalizeKey(encryptionKey); | ||
return createYamlcrypt(normalizedKeys, normalizedEncryptionKey); | ||
} | ||
/** | ||
* Cipher text, encrypted | ||
*/ | ||
class Ciphertext { | ||
constructor(ciphertext, algorithm = null) { | ||
if (algorithm && !algorithms.includes(algorithm)) { | ||
throw new Error(`unsupported algorithm: ${algorithm}`); | ||
} | ||
this.ciphertext = ciphertext; | ||
this.algorithm = algorithm; | ||
} | ||
toString() { | ||
return this.ciphertext; | ||
} | ||
function normalizeKey(key) { | ||
const k = key && key.key ? key.key : key; | ||
if (k == null) { | ||
return null; | ||
} else if (typeof k !== "string") { | ||
throw new Error(`invalid key: ${typeof k}`); | ||
} else if (k.length === 0) { | ||
throw new Error("empty key!"); | ||
} else { | ||
return k; | ||
} | ||
} | ||
/** | ||
* Creates an object for encryption | ||
* @param {string} key The encryption key | ||
* @param {object} opts Encryption options | ||
*/ | ||
function encrypt(key, opts = {}) { | ||
return new _Encryption(key, opts); | ||
function normalizeKeys(keys) { | ||
const arr = Array.isArray(keys) | ||
? keys.map(normalizeKey) | ||
: [normalizeKey(keys)]; | ||
return arr.filter(key => key != null); | ||
} | ||
class _Encryption { | ||
constructor(key, opts) { | ||
opts = opts || {}; | ||
const objects = opts.objects; | ||
const base64 = opts.base64; | ||
this.algorithm = opts.algorithm || DEFAULT_ALGORITHM; | ||
this.types = _types(key, this.algorithm, objects, base64); | ||
this.schema = yaml.Schema.create(this.types); | ||
function createYamlcrypt(keys, encryptionKey) { | ||
function mergeOpts(opts) { | ||
const result = Object.assign({}, opts); | ||
if (!result.hasOwnProperty("keys")) { | ||
result.keys = keys; | ||
} | ||
encryptRaw(str) { | ||
for (const type of this.types) { | ||
if (type.algorithm === this.algorithm) { | ||
return type.represent(str); | ||
} | ||
} | ||
throw new Error('No type found for algorithm: ' + this.algorithm); | ||
if (!result.hasOwnProperty("encryptionKey")) { | ||
result.encryptionKey = encryptionKey; | ||
} | ||
return result; | ||
} | ||
safeDump(obj, opts) { | ||
opts = opts || {}; | ||
opts.schema = this.schema; | ||
return yaml.safeDump(obj, opts); | ||
function trimStr(buf) { | ||
if (Buffer.isBuffer(buf)) { | ||
return buf.toString("ascii").trimRight(); | ||
} else { | ||
return buf.trimRight(); | ||
} | ||
} | ||
safeDumpAll(objs, opts) { | ||
opts = opts || {}; | ||
opts.schema = this.schema; | ||
let str = ''; | ||
for (let idx = 0; idx < objs.length; idx++) { | ||
if (idx > 0) { | ||
str += '---\n'; | ||
} | ||
str += yaml.safeDump(objs[idx], opts); | ||
return { | ||
encrypt: (str, opts = {}) => { | ||
if (opts.raw) { | ||
return encrypt( | ||
opts.algorithm, | ||
opts.encryptionKey || encryptionKey, | ||
str | ||
); | ||
} else { | ||
const obj = yaml.safeLoad(str); | ||
walkStringValues(obj, opts.path, s => new Plaintext(s)); | ||
return yaml.safeDump(obj, yamlOpts(mergeOpts(opts))); | ||
} | ||
}, | ||
encryptAll: (str, opts = {}) => { | ||
if (opts.raw) { | ||
return encrypt( | ||
opts.algorithm, | ||
opts.encryptionKey || encryptionKey, | ||
str | ||
); | ||
} else { | ||
const objs = yaml.safeLoadAll(str); | ||
for (const obj of objs) { | ||
walkStringValues(obj, opts.path, s => new Plaintext(s)); | ||
} | ||
return str; | ||
return safeDumpAll(objs, yamlOpts(mergeOpts(opts))); | ||
} | ||
}, | ||
decrypt: (str, opts = {}) => { | ||
if (opts.raw || isToken(str)) { | ||
const s = trimStr(str); | ||
const decrypted = tryDecrypt( | ||
algorithms, | ||
opts.keys || keys, | ||
(algorithm, key) => decrypt(algorithm, key, s) | ||
); | ||
return yaml.safeLoad(decrypted); | ||
} else { | ||
return yaml.safeLoad(str, yamlOpts(mergeOpts(opts))); | ||
} | ||
}, | ||
decryptAll: (str, opts = {}) => { | ||
if (opts.raw || isToken(str)) { | ||
const s = trimStr(str); | ||
const decrypted = tryDecrypt( | ||
algorithms, | ||
opts.keys || keys, | ||
(algorithm, key) => decrypt(algorithm, key, s) | ||
); | ||
return safeLoadAll(decrypted); | ||
} else { | ||
return safeLoadAll(str, yamlOpts(mergeOpts(opts))); | ||
} | ||
}, | ||
transform: (str, callback, opts = {}) => { | ||
if (opts.raw || isToken(str)) { | ||
const s = trimStr(str); | ||
const [key, algorithm, decrypted] = tryDecrypt( | ||
algorithms, | ||
opts.keys || keys, | ||
(algorithm, key) => [key, algorithm, decrypt(algorithm, key, s)] | ||
); | ||
const transformed = callback(decrypted); | ||
if (transformed.toString() === decrypted) { | ||
return str; | ||
} else { | ||
return encrypt(algorithm, key, transformed); | ||
} | ||
} else { | ||
return doTransform(str, callback, mergeOpts(opts)); | ||
} | ||
} | ||
}; | ||
} | ||
/** | ||
* Creates an object for decryption | ||
* @param {string} key The decryption key | ||
* @param {object} opts Decryption options | ||
*/ | ||
function decrypt(key, opts = {}) { | ||
return new _Decryption(key, opts); | ||
function yamlOpts(opts) { | ||
const schema = createYamlSchema({ | ||
keys: opts.keys, | ||
encryptionKey: opts.encryptionKey, | ||
algorithm: opts.algorithm, | ||
objects: !!opts.objects, | ||
base64: !!opts.base64 | ||
}); | ||
return { schema }; | ||
} | ||
class _Decryption { | ||
constructor(key, opts) { | ||
opts = opts || {}; | ||
const objects = opts.objects; | ||
const base64 = opts.base64; | ||
this.types = _types(key, null, objects, base64); | ||
this.schema = yaml.Schema.create(this.types); | ||
} | ||
function createYamlSchema({ algorithm, keys, encryptionKey, objects, base64 }) { | ||
const opts = { keys, encryptionKey, objects, base64 }; | ||
const types = []; | ||
for (let i = 0; i < algorithms.length; i++) { | ||
const isDefault = | ||
algorithms[i] === algorithm || (algorithm == null && i === 0); | ||
types.push( | ||
createType( | ||
Object.assign({}, opts, { | ||
algorithm: algorithms[i], | ||
isDefault | ||
}) | ||
) | ||
); | ||
} | ||
return yaml.Schema.create(types); | ||
} | ||
decryptRaw(str) { | ||
for (const type of this.types) { | ||
try { | ||
return type.construct(str); | ||
} catch (e) { | ||
continue; | ||
} | ||
} | ||
throw new Error('No algorithm found to decrypt message!'); | ||
function createType({ | ||
algorithm, | ||
isDefault, | ||
keys, | ||
encryptionKey, | ||
objects, | ||
base64 | ||
}) { | ||
const name = "!yaml-crypt" + (algorithm == null ? "" : `/${algorithm}`); | ||
const type = new yaml.Type(name, { | ||
kind: "scalar", | ||
instanceOf: Plaintext, | ||
resolve: data => data !== null, | ||
construct: data => { | ||
const decrypted = tryDecrypt([algorithm], keys, (algorithm, key) => | ||
decrypt(algorithm, key, data) | ||
); | ||
const decoded = base64 | ||
? Buffer.from(decrypted, "base64").toString("utf8") | ||
: decrypted; | ||
return objects ? new Plaintext(decoded, data, algorithm) : decoded; | ||
}, | ||
predicate: data => { | ||
return ( | ||
data.algorithm === algorithm || (data.algorithm == null && isDefault) | ||
); | ||
}, | ||
represent: data => { | ||
let encrypted; | ||
if (data.ciphertext) { | ||
encrypted = data.ciphertext; | ||
} else { | ||
const str = data.toString(); | ||
const encoded = base64 ? Buffer.from(str).toString("base64") : str; | ||
encrypted = encrypt(algorithm, encryptionKey, encoded); | ||
} | ||
return encrypted; | ||
} | ||
}); | ||
return type; | ||
} | ||
safeLoad(str, opts) { | ||
opts = opts || {}; | ||
opts.schema = this.schema; | ||
return yaml.safeLoad(str, opts); | ||
} | ||
function doTransform(str, callback, opts) { | ||
const [key, docs] = tryDecrypt(algorithms, opts.keys, (algorithm, key) => { | ||
const o = Object.assign({}, opts); | ||
o.objects = true; | ||
o.algorithm = algorithm; | ||
o.keys = [key]; | ||
const docs = safeLoadAll(str, yamlOpts(o)); | ||
return [key, docs]; | ||
}); | ||
safeLoadAll(str, iterator, opts) { | ||
opts = opts || {}; | ||
opts.schema = this.schema; | ||
return yaml.safeLoadAll(str, iterator, opts); | ||
if (!opts.encryptionKey) { | ||
opts.encryptionKey = key; | ||
} | ||
const reencrypting = key !== opts.encryptionKey; | ||
let index = 0; | ||
const types = []; | ||
for (const doc of docs) { | ||
walkValues( | ||
doc, | ||
null, | ||
v => v instanceof Plaintext, | ||
t => { | ||
const knownText = new KnownText(t.algorithm, t, index++); | ||
types.push(knownTextType(knownText, reencrypting)); | ||
return knownText; | ||
} | ||
); | ||
} | ||
newTextTypes().forEach(t => types.push(t)); | ||
const schema = yaml.Schema.create(types); | ||
const decrypted = safeDumpAll(docs, { schema: schema }); | ||
const transformed = callback(decrypted); | ||
const result = safeLoadAll(transformed, { schema: schema }); | ||
return safeDumpAll(result, yamlOpts(opts)); | ||
} | ||
function knownTextType(knownText, reencrypt) { | ||
return new yaml.Type(`!yaml-crypt/:${knownText.index}`, { | ||
kind: "scalar", | ||
instanceOf: KnownText, | ||
predicate: data => data.index === knownText.index, | ||
represent: data => data.plaintext.plaintext, | ||
construct: data => { | ||
if (reencrypt || data !== knownText.plaintext.plaintext) { | ||
return new Plaintext(data, null, knownText.algorithm); | ||
} else { | ||
return knownText.plaintext; | ||
} | ||
} | ||
}); | ||
} | ||
/** | ||
* Return an array of custom Yaml types | ||
* @param {string} key Encryption/decryption key | ||
* @param {string} defaultAlgorithm Default algorithm | ||
* @param {boolean} objects Should objects or strings be returned? | ||
* @param {boolean} base64 Should the strings be base64 encoded? | ||
*/ | ||
function _types(key, defaultAlgorithm, objects, base64) { | ||
defaultAlgorithm = defaultAlgorithm || DEFAULT_ALGORITHM; | ||
const fernet = _cryptoType(ALGORITHM_FERNET, key, defaultAlgorithm === ALGORITHM_FERNET, | ||
objects, base64, crypto.fernetEncrypt, crypto.fernetDecrypt); | ||
const branca = _cryptoType(ALGORITHM_BRANCA, key, defaultAlgorithm === ALGORITHM_BRANCA, | ||
objects, base64, crypto.brancaEncrypt, crypto.brancaDecrypt); | ||
return [fernet, branca]; | ||
function newTextTypes() { | ||
const keys = [{ type: "!yaml-crypt", algorithm: algorithms[0] }]; | ||
for (const algorithm of algorithms) { | ||
// also allow the usage of just the algorithm name, without version: | ||
const split = algorithm.split(":", 2); | ||
keys.push({ type: `!yaml-crypt/${split[0]}`, algorithm: algorithm }); | ||
keys.push({ type: `!yaml-crypt/${algorithm}`, algorithm: algorithm }); | ||
} | ||
return keys.map( | ||
key => | ||
new yaml.Type(key.type, { | ||
kind: "scalar", | ||
construct: data => new Plaintext(data, null, key.algorithm) | ||
}) | ||
); | ||
} | ||
function _cryptoType(algorithm, key, isDefault, objects, base64, encrypt, decrypt) { | ||
const type = new yaml.Type('!yaml-crypt/' + algorithm, { | ||
kind: 'scalar', | ||
instanceOf: Plaintext, | ||
resolve: (data) => data !== null, | ||
construct: data => { | ||
const decrypted = decrypt(key, data); | ||
const decoded = (base64 ? Buffer.from(decrypted, 'base64').toString() : decrypted); | ||
return (objects ? new Plaintext(decoded, data, algorithm) : decoded); | ||
}, | ||
predicate: data => { | ||
return data.algorithm === algorithm || (!data.algorithm && isDefault); | ||
}, | ||
represent: data => { | ||
let encrypted; | ||
if (data.ciphertext) { | ||
encrypted = data.ciphertext; | ||
} else { | ||
const str = data.toString(); | ||
const encoded = (base64 ? Buffer.from(str).toString('base64') : str); | ||
encrypted = encrypt(key, encoded); | ||
} | ||
return (objects ? new Ciphertext(encrypted) : encrypted); | ||
} | ||
}); | ||
type.algorithm = algorithm; | ||
return type; | ||
class Plaintext { | ||
constructor(plaintext, ciphertext = null, algorithm = null) { | ||
this.plaintext = plaintext; | ||
this.ciphertext = ciphertext; | ||
this.algorithm = algorithm; | ||
} | ||
toString() { | ||
return this.plaintext; | ||
} | ||
} | ||
module.exports.algorithms = algorithms; | ||
module.exports.generateKey = generateKey; | ||
module.exports.Plaintext = Plaintext; | ||
module.exports.Ciphertext = Ciphertext; | ||
module.exports.encrypt = encrypt; | ||
module.exports.decrypt = decrypt; | ||
class KnownText { | ||
constructor(algorithm, plaintext, index) { | ||
this.algorithm = algorithm; | ||
this.plaintext = plaintext; | ||
this.index = index; | ||
} | ||
} | ||
module.exports = { | ||
algorithms, | ||
generateKey, | ||
loadConfig, | ||
yamlcrypt, | ||
encrypt, | ||
decrypt | ||
}; |
{ | ||
"name": "yaml-crypt", | ||
"version": "0.3.4", | ||
"version": "0.4.0", | ||
"description": "Encrypt and decrypt YAML documents", | ||
@@ -13,5 +13,14 @@ "license": "MIT", | ||
"main": "./lib/yaml-crypt.js", | ||
"types": "./lib/index.d.ts", | ||
"bin": { | ||
"yaml-crypt": "./bin/yaml-crypt-cli.js" | ||
}, | ||
"scripts": { | ||
"lint": "eslint . && prettier -l */**.js */**.ts", | ||
"test": "nyc mocha --timeout=8000 --check-leaks", | ||
"test:coverage": "npx nyc --reporter=lcov mocha", | ||
"build": "yarn lint && yarn test", | ||
"prepublish": "yarn build", | ||
"coverage": "nyc report --reporter=text-lcov | coveralls" | ||
}, | ||
"dependencies": { | ||
@@ -26,15 +35,10 @@ "argparse": "^1.0.7", | ||
}, | ||
"scripts": { | ||
"lint": "eslint .", | ||
"test": "nyc mocha --timeout=8000 --check-leaks", | ||
"build": "yarn lint && yarn test", | ||
"coverage": "nyc report --reporter=text-lcov | coveralls" | ||
}, | ||
"devDependencies": { | ||
"chai": "^4.1.2", | ||
"coveralls": "^3.0.0", | ||
"eslint": "^5.5.0", | ||
"eslint": "^5.11.0", | ||
"mocha": "^5.1.0", | ||
"nyc": "^13.0.1" | ||
"nyc": "^13.0.1", | ||
"prettier": "^1.15.3" | ||
} | ||
} |
@@ -91,4 +91,11 @@ # yaml-crypt | ||
## Related projects | ||
- https://github.com/mozilla/sops | ||
- https://github.com/huwtl/secure_yaml | ||
- https://github.com/StackExchange/blackbox | ||
- https://github.com/bitnami-labs/sealed-secrets | ||
## License | ||
The yaml-crypt tool is licensed under the MIT License |
@@ -1,33 +0,95 @@ | ||
const mocha = require('mocha'); | ||
const fs = require("fs"); | ||
const mocha = require("mocha"); | ||
const describe = mocha.describe; | ||
const it = mocha.it; | ||
const chai = require('chai'); | ||
const chai = require("chai"); | ||
const expect = chai.expect; | ||
const crypto = require('../lib/crypto'); | ||
const crypto = require("../lib/crypto"); | ||
require('./crypto-util').setupCrypto(); | ||
require("./crypto-util").setupCrypto(); | ||
describe('crypto', () => { | ||
it('should return the encrypted content (fernet)', () => { | ||
const result = crypto.fernetEncrypt('aehae5Ui0Eechaeghau9Yoh9jufiep7H', 'Hello, world!'); | ||
expect(result).to.equal('gAAAAAAAAAABAAECAwQFBgcICQoLDA0OD7nQ_JQsjDx78n7mQ9bW3T-rgiTN7WX3Uq66EDA0qxZDNQppXL6WaOAIW4x8ElmcRg=='); | ||
}); | ||
describe("crypto", () => { | ||
it("should return the encrypted content (fernet)", () => { | ||
const result = crypto.encrypt( | ||
"fernet", | ||
"aehae5Ui0Eechaeghau9Yoh9jufiep7H", | ||
"Hello, world!" | ||
); | ||
expect(result).to.equal( | ||
"gAAAAAAAAAABAAECAwQFBgcICQoLDA0OD7nQ_JQsjDx78n7mQ9bW3T-rgiTN7WX3Uq66EDA0qxZDNQppXL6WaOAIW4x8ElmcRg==" | ||
); | ||
}); | ||
it('should return the encrypted content (branca)', () => { | ||
const result = crypto.brancaEncrypt('aehae5Ui0Eechaeghau9Yoh9jufiep7H', 'Hello, world!'); | ||
expect(result).to.equal('XUvrtHkyXTh1VUW885Ta4V5eQ3hBMFQMC3S3QwEfWzKWVDt3A5TnVUNtVXubi0fsAA8eerahpobwC8'); | ||
}); | ||
it("should return the encrypted content (branca)", () => { | ||
const result = crypto.encrypt( | ||
"branca", | ||
"aehae5Ui0Eechaeghau9Yoh9jufiep7H", | ||
"Hello, world!" | ||
); | ||
expect(result).to.equal( | ||
"XUvrtHkyXTh1VUW885Ta4V5eQ3hBMFQMC3S3QwEfWzKWVDt3A5TnVUNtVXubi0fsAA8eerahpobwC8" | ||
); | ||
}); | ||
it('should return the decrypted content (fernet)', () => { | ||
const key = 'aehae5Ui0Eechaeghau9Yoh9jufiep7H'; | ||
const encrypted = 'gAAAAAAAAAABAAECAwQFBgcICQoLDA0OD7nQ_JQsjDx78n7mQ9bW3T-rgiTN7WX3Uq66EDA0qxZDNQppXL6WaOAIW4x8ElmcRg=='; | ||
expect(crypto.fernetDecrypt(key, encrypted)).to.equal('Hello, world!'); | ||
}); | ||
it("should generate a key (branca)", () => { | ||
const result = crypto.generateKey("branca"); | ||
expect(result).to.have.lengthOf(32); | ||
}); | ||
it('should return the decrypted content (branca)', () => { | ||
const key = 'aehae5Ui0Eechaeghau9Yoh9jufiep7H'; | ||
const encrypted = 'XUvrtHkyXTh1VUW885Ta4V5eQ3hBMFQMC3S3QwEfWzKWVDt3A5TnVUNtVXubi0fsAA8eerahpobwC8'; | ||
expect(crypto.brancaDecrypt(key, encrypted)).to.equal('Hello, world!'); | ||
}); | ||
it("should throw an error when passing null", () => { | ||
expect(() => crypto.decrypt("fernet", "", null)).to.throw( | ||
/message is null/ | ||
); | ||
}); | ||
it("should throw an error when passing invalid data", () => { | ||
expect(() => crypto.decrypt("fernet", "", {})).to.throw( | ||
/invalid type for message/ | ||
); | ||
}); | ||
it("should throw an error when passing invalid algorithm", () => { | ||
expect(() => crypto.decrypt("x", "", "")).to.throw(/unknown algorithm/); | ||
}); | ||
it("should return the decrypted content (fernet)", () => { | ||
const key = "aehae5Ui0Eechaeghau9Yoh9jufiep7H"; | ||
const encrypted = | ||
"gAAAAAAAAAABAAECAwQFBgcICQoLDA0OD7nQ_JQsjDx78n7mQ9bW3T-rgiTN7WX3Uq66EDA0qxZDNQppXL6WaOAIW4x8ElmcRg=="; | ||
expect(crypto.decrypt("fernet", key, encrypted)).to.equal("Hello, world!"); | ||
}); | ||
it("should return the decrypted content (branca)", () => { | ||
const key = "aehae5Ui0Eechaeghau9Yoh9jufiep7H"; | ||
const encrypted = | ||
"XUvrtHkyXTh1VUW885Ta4V5eQ3hBMFQMC3S3QwEfWzKWVDt3A5TnVUNtVXubi0fsAA8eerahpobwC8"; | ||
expect(crypto.decrypt("branca", key, encrypted)).to.equal("Hello, world!"); | ||
}); | ||
it("should correctly identify valid tokens (fernet)", () => { | ||
expect(crypto.isToken("gAAAAAAAAAA")).to.equal(true); | ||
expect(crypto.isToken("gBBBB")).to.equal(true); | ||
}); | ||
it("should correctly identify valid Buffer tokens (fernet)", () => { | ||
expect(crypto.isToken(Buffer.from("gAAAAAAAAAA"))).to.equal(true); | ||
expect(crypto.isToken(Buffer.from("gBBBB"))).to.equal(true); | ||
}); | ||
it("should correctly identify valid string tokens (branca)", () => { | ||
const token = fs.readFileSync("./test/test-7.yaml-crypt").toString(); | ||
expect(crypto.isToken(token)).to.equal(true); | ||
}); | ||
it("should correctly identify valid Buffer tokens (branca)", () => { | ||
const token = fs.readFileSync("./test/test-7.yaml-crypt"); | ||
expect(crypto.isToken(token)).to.equal(true); | ||
}); | ||
it("should correctly identify invalid tokens", () => { | ||
expect(crypto.isToken("X")).to.equal(false); | ||
expect(crypto.isToken("XXX")).to.equal(false); | ||
}); | ||
}); |
@@ -1,4 +0,4 @@ | ||
const crypto = require('../lib/crypto'); | ||
const crypto = require("../lib/crypto"); | ||
const fernet = require('fernet'); | ||
const fernet = require("fernet"); | ||
@@ -9,18 +9,18 @@ /** | ||
function setupCrypto() { | ||
// Fernet: | ||
const setIV = fernet.Token.prototype.setIV; | ||
fernet.Token.prototype.setIV = function () { | ||
const iv = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; | ||
setIV.apply(this, [iv]); | ||
}; | ||
const setTime = fernet.Token.prototype.setTime; | ||
fernet.Token.prototype.setTime = function () { | ||
setTime.apply(this, [1000]); | ||
}; | ||
// Fernet: | ||
const setIV = fernet.Token.prototype.setIV; | ||
fernet.Token.prototype.setIV = function() { | ||
const iv = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; | ||
setIV.apply(this, [iv]); | ||
}; | ||
const setTime = fernet.Token.prototype.setTime; | ||
fernet.Token.prototype.setTime = function() { | ||
setTime.apply(this, [1000]); | ||
}; | ||
// Branca: | ||
crypto.brancaDefaults.ts = 1; | ||
crypto.brancaDefaults.nonce = Buffer.alloc(24, 1); | ||
// Branca: | ||
crypto.__brancaDefaults.ts = 1; | ||
crypto.__brancaDefaults.nonce = Buffer.alloc(24, 1); | ||
} | ||
module.exports.setupCrypto = setupCrypto; |
@@ -1,236 +0,323 @@ | ||
const fs = require('fs'); | ||
const fs = require("fs"); | ||
const mocha = require('mocha'); | ||
const mocha = require("mocha"); | ||
const describe = mocha.describe; | ||
const it = mocha.it; | ||
const chai = require('chai'); | ||
const chai = require("chai"); | ||
const expect = chai.expect; | ||
const tmp = require('tmp'); | ||
const yaml = require('js-yaml'); | ||
const tmp = require("tmp"); | ||
const yaml = require("js-yaml"); | ||
const yamlcryptcli = require('../bin/yaml-crypt-cli'); | ||
const yamlcryptcli = require("../bin/yaml-crypt-cli"); | ||
require('./crypto-util').setupCrypto(); | ||
require("./crypto-util").setupCrypto(); | ||
class Out { | ||
constructor() { | ||
this.str = ''; | ||
} | ||
constructor() { | ||
this.str = ""; | ||
} | ||
write(obj) { | ||
if (typeof obj === 'string') { | ||
this.str += obj; | ||
} else { | ||
this.str += obj.toString(); | ||
} | ||
write(obj) { | ||
if (typeof obj === "string") { | ||
this.str += obj; | ||
} else { | ||
this.str += obj.toString(); | ||
} | ||
} | ||
} | ||
describe('yaml-crypt-cli', () => { | ||
it('should throw an error when using --unknown-option', () => { | ||
expect(() => yamlcryptcli.run(['--unknown-option'], {}, { 'stdout': new Out() })) | ||
.to.throw().with.property('status', 2); | ||
}); | ||
describe("yaml-crypt-cli", () => { | ||
it("should display a message about details when giving -h", () => { | ||
const stdout = new Out(); | ||
try { | ||
yamlcryptcli.run(["-h"], {}, { stdout }); | ||
} catch (e) { | ||
// ignore | ||
} | ||
expect(stdout.str).to.contain("For more details, specify --help"); | ||
}); | ||
it('should display usage info when using --help', () => { | ||
expect(() => yamlcryptcli.run(['--help'], {}, { 'stdout': new Out() })) | ||
.to.throw().with.property('status', 0); | ||
}); | ||
it("should throw an error when using --unknown-option", () => { | ||
expect(() => | ||
yamlcryptcli.run(["--unknown-option"], {}, { stdout: new Out() }) | ||
) | ||
.to.throw() | ||
.with.property("status", 2); | ||
}); | ||
it('should throw an error when using --path and --raw', () => { | ||
expect(() => runWithKeyFile(['--path', 'x', '--raw'], {}, { 'stdout': new Out() })) | ||
.to.throw(/cannot combine/); | ||
}); | ||
it("should display usage info when using --help", () => { | ||
expect(() => yamlcryptcli.run(["--help"], {}, { stdout: new Out() })) | ||
.to.throw() | ||
.with.property("status", 0); | ||
}); | ||
it('should throw an error when passing directory without --dir', () => { | ||
expect(() => runWithKeyFile(['.'], {}, { 'stdout': new Out() })) | ||
.to.throw(/directories will be skipped/); | ||
}); | ||
it("should throw an error when combining invalid flags", () => { | ||
const invalid = [ | ||
["--raw", "--path", "."], | ||
["--edit", "--encrypt"], | ||
["--edit", "--decrypt"], | ||
["--edit", "--keep"], | ||
["--generate-key", "-e"], | ||
["--generate-key", "-d"] | ||
]; | ||
for (const args of invalid) { | ||
expect(() => runWithKeyFile(args, {}, {})).to.throw(/cannot combine/); | ||
} | ||
}); | ||
it('should throw an error when passing non-existing files to --edit', () => { | ||
expect(() => runWithKeyFile(['--edit', 'x.yaml-crypt'], {}, { 'stdout': new Out() })) | ||
.to.throw(/file does not exist/); | ||
}); | ||
it("should throw an error when passing directory without --dir", () => { | ||
expect(() => runWithKeyFile(["."], {}, { stdout: new Out() })).to.throw( | ||
/directories will be skipped/ | ||
); | ||
}); | ||
it('should throw an error when encrypting with two keys', () => { | ||
const secondKeyFile = tmp.fileSync(); | ||
fs.writeSync(secondKeyFile.fd, 'aehae5Ui0Eechaeghau9Yoh9jufiep72'); | ||
expect(() => runWithKeyFile(['-k', secondKeyFile.name, '-e'], {}, { 'stdout': new Out() })) | ||
.to.throw(/encrypting, but multiple keys given/); | ||
}); | ||
it("should throw an error when passing non-existing files to --edit", () => { | ||
expect(() => | ||
runWithKeyFile(["--edit", "x.yaml-crypt"], {}, { stdout: new Out() }) | ||
).to.throw(/file does not exist/); | ||
}); | ||
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 invalid algorithm", () => { | ||
expect(() => runWithKeyFile(["-d", "-a", "x"], {}, {})).to.throw( | ||
/unknown encryption algorithm/ | ||
); | ||
}); | ||
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 throw an error when encrypting with two keys", () => { | ||
const secondKeyFile = tmp.fileSync(); | ||
fs.writeSync(secondKeyFile.fd, "aehae5Ui0Eechaeghau9Yoh9jufiep72"); | ||
expect(() => | ||
runWithKeyFile( | ||
["-k", secondKeyFile.name, "-e"], | ||
{}, | ||
{ stdout: new Out() } | ||
) | ||
).to.throw(/encrypting, but multiple keys given/); | ||
}); | ||
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 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 encrypt the given YAML file (fernet)', () => { | ||
const input = tmp.fileSync({ 'postfix': '.yaml' }); | ||
fs.copyFileSync('./test/test-2.yaml', input.name); | ||
runWithKeyFile([input.name], {}, { 'stdout': new Out() }); | ||
const output = fs.readFileSync(input.name + '-crypt'); | ||
const expected = fs.readFileSync('./test/test-2a.yaml-crypt'); | ||
expect(output.toString('utf8')).to.equal(expected.toString('utf8')); | ||
}); | ||
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 encrypt the given YAML file (branca)', () => { | ||
const input = tmp.fileSync({ 'postfix': '.yaml' }); | ||
fs.copyFileSync('./test/test-2.yaml', input.name); | ||
runWithKeyFile(['-a', 'branca', input.name], {}, { 'stdout': new Out() }); | ||
const output = fs.readFileSync(input.name + '-crypt'); | ||
const expected = fs.readFileSync('./test/test-2b.yaml-crypt'); | ||
expect(output.toString('utf8')).to.equal(expected.toString('utf8')); | ||
}); | ||
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 decrypt the given YAML file', () => { | ||
const input = tmp.fileSync({ 'postfix': '.yaml-crypt' }); | ||
fs.copyFileSync('./test/test-2a.yaml-crypt', input.name); | ||
runWithKeyFile([input.name], {}, { 'stdout': new Out() }); | ||
const output = fs.readFileSync(input.name.substring(0, input.name.length - '-crypt'.length)); | ||
const expected = fs.readFileSync('./test/test-2.yaml'); | ||
expect(output.toString('utf8')).to.equal(expected.toString('utf8')); | ||
}); | ||
it("should encrypt the given YAML file (fernet)", () => { | ||
const input = tmp.fileSync({ postfix: ".yaml" }); | ||
fs.copyFileSync("./test/test-2.yaml", input.name); | ||
runWithKeyFile([input.name], {}, { stdout: new Out() }); | ||
const output = fs.readFileSync(input.name + "-crypt"); | ||
const expected = fs.readFileSync("./test/test-2a.yaml-crypt"); | ||
expect(output.toString("utf8")).to.equal(expected.toString("utf8")); | ||
}); | ||
it('should encrypt only parts of the YAML file when using --path', () => { | ||
const input = tmp.fileSync({ 'postfix': '.yaml' }); | ||
fs.writeSync(input.fd, yaml.safeDump({ 'a': { 'b': { 'c': 'secret' } }, 'x': 'plain' })); | ||
runWithKeyFile(['--path', 'a.b.c', input.name], {}, { 'stdout': new Out() }); | ||
const output = fs.readFileSync(input.name + '-crypt'); | ||
const expected = fs.readFileSync('./test/test-3.yaml-crypt'); | ||
expect(output.toString('utf8')).to.equal(expected.toString('utf8')); | ||
}); | ||
it("should encrypt the given YAML file (branca)", () => { | ||
const input = tmp.fileSync({ postfix: ".yaml" }); | ||
fs.copyFileSync("./test/test-2.yaml", input.name); | ||
runWithKeyFile(["-a", "branca", input.name], {}, { stdout: new Out() }); | ||
const output = fs.readFileSync(input.name + "-crypt"); | ||
const expected = fs.readFileSync("./test/test-2b.yaml-crypt"); | ||
expect(output.toString("utf8")).to.equal(expected.toString("utf8")); | ||
}); | ||
it('should remove the old files', () => { | ||
const input = tmp.fileSync({ 'postfix': '.yaml' }); | ||
fs.copyFileSync('./test/test-2.yaml', input.name); | ||
runWithKeyFile([input.name], {}, { 'stdout': new Out() }); | ||
expect(fs.existsSync(input.name)).to.equal(false); | ||
}); | ||
it("should decrypt the given YAML file", () => { | ||
const input = tmp.fileSync({ postfix: ".yaml-crypt" }); | ||
fs.copyFileSync("./test/test-2a.yaml-crypt", input.name); | ||
runWithKeyFile([input.name], {}, { stdout: new Out() }); | ||
const output = fs.readFileSync( | ||
input.name.substring(0, input.name.length - "-crypt".length) | ||
); | ||
const expected = fs.readFileSync("./test/test-2.yaml"); | ||
expect(output.toString("utf8")).to.equal(expected.toString("utf8")); | ||
}); | ||
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); | ||
}); | ||
it("should decrypt the given directory", () => { | ||
const tmpdir = tmp.dirSync(); | ||
fs.copyFileSync("./test/test-2a.yaml-crypt", `${tmpdir.name}/1.yaml-crypt`); | ||
fs.copyFileSync("./test/test-2a.yaml-crypt", `${tmpdir.name}/2.yml-crypt`); | ||
runWithKeyFile(["--dir", tmpdir.name], {}, { stdout: new Out() }); | ||
const expected = fs.readFileSync("./test/test-2.yaml"); | ||
const output1 = fs.readFileSync(`${tmpdir.name}/1.yaml`); | ||
const output2 = fs.readFileSync(`${tmpdir.name}/2.yml`); | ||
expect(output1.toString("utf8")).to.equal(expected.toString("utf8")); | ||
expect(output2.toString("utf8")).to.equal(expected.toString("utf8")); | ||
}); | ||
function runWithKeyFile(argv, config, options) { | ||
const keyFile = tmp.fileSync(); | ||
fs.writeSync(keyFile.fd, 'aehae5Ui0Eechaeghau9Yoh9jufiep7H'); | ||
return yamlcryptcli.run(['--debug', '-k', keyFile.name].concat(argv), config, options); | ||
} | ||
it("should encrypt only parts of the YAML file when using --path", () => { | ||
const input = tmp.fileSync({ postfix: ".yaml" }); | ||
fs.writeSync( | ||
input.fd, | ||
yaml.safeDump({ a: { b: { c: "secret" } }, x: "plain" }) | ||
); | ||
runWithKeyFile(["--path", "a.b.c", input.name], {}, { stdout: new Out() }); | ||
const output = fs.readFileSync(input.name + "-crypt"); | ||
const expected = fs.readFileSync("./test/test-3.yaml-crypt"); | ||
expect(output.toString("utf8")).to.equal(expected.toString("utf8")); | ||
}); | ||
it('should throw an error when no matching key is available', () => { | ||
const keyFile = tmp.fileSync(); | ||
fs.writeSync(keyFile.fd, 'INVALID_KEYchaeghau9Yoh9jufiep7H'); | ||
const input = tmp.fileSync({ 'postfix': '.yaml-crypt' }); | ||
fs.copyFileSync('./test/test-2a.yaml-crypt', input.name); | ||
expect(() => yamlcryptcli.run(['-k', keyFile.name, input.name], {}, { 'stdout': new Out() })) | ||
.to.throw(/no matching key/); | ||
}); | ||
it("should remove the old files", () => { | ||
const input = tmp.fileSync({ postfix: ".yaml" }); | ||
fs.copyFileSync("./test/test-2.yaml", input.name); | ||
runWithKeyFile([input.name], {}, { stdout: new Out() }); | ||
expect(fs.existsSync(input.name)).to.equal(false); | ||
}); | ||
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 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); | ||
}); | ||
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/); | ||
}); | ||
function runWithKeyFile(argv, config, options) { | ||
const keyFile = tmp.fileSync(); | ||
fs.writeSync(keyFile.fd, "aehae5Ui0Eechaeghau9Yoh9jufiep7H"); | ||
return yamlcryptcli.run( | ||
["--debug", "-k", keyFile.name].concat(argv), | ||
config, | ||
options | ||
); | ||
} | ||
it('should decrypt the given input', () => { | ||
const config = { | ||
'keys': [ | ||
{ 'key': 'INVALID_KEY____________________X' }, | ||
{ 'key': 'aehae5Ui0Eechaeghau9Yoh9jufiep7H' } | ||
] | ||
}; | ||
const options = { | ||
'stdin': fs.readFileSync('./test/test-2a.yaml-crypt'), | ||
'stdout': new Out() | ||
}; | ||
yamlcryptcli.run(['-d'], config, options); | ||
const expected = fs.readFileSync('./test/test-2.yaml').toString(); | ||
expect(options.stdout.str).to.equal(expected); | ||
}); | ||
it("should decrypt the given YAML file (key passed via fd)", () => { | ||
const input = tmp.fileSync({ postfix: ".yaml-crypt" }); | ||
fs.copyFileSync("./test/test-2a.yaml-crypt", input.name); | ||
const keyFile = tmp.fileSync(); | ||
fs.writeFileSync(keyFile.name, "aehae5Ui0Eechaeghau9Yoh9jufiep7H"); | ||
const fd = fs.openSync(keyFile.name, "r"); | ||
yamlcryptcli.run( | ||
["--debug", "-k", `fd:${fd}`, input.name], | ||
{}, | ||
{ stdout: new Out() } | ||
); | ||
const output = fs.readFileSync( | ||
input.name.substring(0, input.name.length - "-crypt".length) | ||
); | ||
const expected = fs.readFileSync("./test/test-2.yaml"); | ||
expect(output.toString("utf8")).to.equal(expected.toString("utf8")); | ||
}); | ||
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': Buffer.from(input), | ||
'stdout': new Out() | ||
}; | ||
yamlcryptcli.run(['-d', '--raw'], config, options); | ||
expect(options.stdout.str).to.equal('Hello, world!'); | ||
}); | ||
it("should throw an error when no matching key is available", () => { | ||
const keyFile = tmp.fileSync(); | ||
fs.writeSync(keyFile.fd, "INVALID_KEYchaeghau9Yoh9jufiep7H"); | ||
const input = tmp.fileSync({ postfix: ".yaml-crypt" }); | ||
fs.copyFileSync("./test/test-2a.yaml-crypt", input.name); | ||
expect(() => | ||
yamlcryptcli.run( | ||
["-k", keyFile.name, input.name], | ||
{}, | ||
{ stdout: new Out() } | ||
) | ||
).to.throw(/no matching key/); | ||
}); | ||
it('should encrypt the whole input when using --raw', () => { | ||
const config = { | ||
'keys': [ | ||
{ 'key': 'KEY_THAT_SHOULD_NOT_BE_USED_____', 'name': 'key1' }, | ||
{ 'key': 'aehae5Ui0Eechaeghau9Yoh9jufiep7H', 'name': 'key2' } | ||
] | ||
}; | ||
const options = { | ||
'stdin': 'Hello, world!', | ||
'stdout': new Out() | ||
}; | ||
yamlcryptcli.run(['-e', '--raw', '-K', 'c:key2'], config, options); | ||
const expected = 'gAAAAAAAAAABAAECAwQFBgcICQoLDA0OD7nQ_JQsjDx78n7mQ9bW3T-rgiTN7WX3Uq66EDA0qxZDNQppXL6WaOAIW4x8ElmcRg==\n'; | ||
expect(options.stdout.str).to.equal(expected); | ||
}); | ||
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 return the same YAML file when using --edit and not changing anything', () => { | ||
const keyFile = tmp.fileSync(); | ||
fs.writeSync(keyFile.fd, 'aehae5Ui0Eechaeghau9Yoh9jufiep7H'); | ||
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/ | ||
); | ||
}); | ||
const input = tmp.fileSync({ 'postfix': '.yaml-crypt' }); | ||
fs.copyFileSync('./test/test-2a.yaml-crypt', input.name); | ||
it("should decrypt the given input", () => { | ||
const config = { | ||
keys: [ | ||
{ key: "INVALID_KEY____________________X" }, | ||
{ key: "aehae5Ui0Eechaeghau9Yoh9jufiep7H" } | ||
] | ||
}; | ||
const options = { | ||
stdin: fs.readFileSync("./test/test-2a.yaml-crypt"), | ||
stdout: new Out() | ||
}; | ||
yamlcryptcli.run(["-d"], config, options); | ||
const expected = fs.readFileSync("./test/test-2.yaml").toString(); | ||
expect(options.stdout.str).to.equal(expected); | ||
}); | ||
yamlcryptcli.run(['--debug', '-k', keyFile.name, '--edit', input.name], { 'editor': 'touch' }, {}); | ||
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: Buffer.from(input), | ||
stdout: new Out() | ||
}; | ||
yamlcryptcli.run(["-d", "--raw"], config, options); | ||
expect(options.stdout.str).to.equal("Hello, world!"); | ||
}); | ||
const output = fs.readFileSync(input.name); | ||
const expected = fs.readFileSync('./test/test-2a.yaml-crypt'); | ||
expect(output.toString('utf8')).to.equal(expected.toString('utf8')); | ||
}); | ||
it("should encrypt the whole input when using --raw", () => { | ||
const config = { | ||
keys: [ | ||
{ key: "KEY_THAT_SHOULD_NOT_BE_USED_____", name: "key1" }, | ||
{ key: "aehae5Ui0Eechaeghau9Yoh9jufiep7H", name: "key2" } | ||
] | ||
}; | ||
const options = { | ||
stdin: "Hello, world!", | ||
stdout: new Out() | ||
}; | ||
yamlcryptcli.run(["-e", "--raw", "-K", "c:key2"], config, options); | ||
const expected = | ||
"gAAAAAAAAAABAAECAwQFBgcICQoLDA0OD7nQ_JQsjDx78n7mQ9bW3T-rgiTN7WX3Uq66EDA0qxZDNQppXL6WaOAIW4x8ElmcRg==\n"; | ||
expect(options.stdout.str).to.equal(expected); | ||
}); | ||
it("should return the same YAML file when using --edit and not changing anything", () => { | ||
const keyFile = tmp.fileSync(); | ||
fs.writeSync(keyFile.fd, "aehae5Ui0Eechaeghau9Yoh9jufiep7H"); | ||
const input = tmp.fileSync({ postfix: ".yaml-crypt" }); | ||
fs.copyFileSync("./test/test-2a.yaml-crypt", input.name); | ||
yamlcryptcli.run( | ||
["--debug", "-k", keyFile.name, "--edit", input.name], | ||
{ editor: "touch" }, | ||
{} | ||
); | ||
const output = fs.readFileSync(input.name); | ||
const expected = fs.readFileSync("./test/test-2a.yaml-crypt"); | ||
expect(output.toString("utf8")).to.equal(expected.toString("utf8")); | ||
}); | ||
}); |
@@ -1,44 +0,160 @@ | ||
const fs = require('fs'); | ||
const fs = require("fs"); | ||
const mocha = require('mocha'); | ||
const mocha = require("mocha"); | ||
const describe = mocha.describe; | ||
const it = mocha.it; | ||
const chai = require('chai'); | ||
const chai = require("chai"); | ||
const expect = chai.expect; | ||
const yamlcrypt = require('../lib/yaml-crypt'); | ||
const tmp = require("tmp"); | ||
require('./crypto-util').setupCrypto(); | ||
const { loadConfig, yamlcrypt } = require("../lib/yaml-crypt"); | ||
describe('yaml-crypt', () => { | ||
it('should read the decrypted content', () => { | ||
const yaml = yamlcrypt.decrypt('aehae5Ui0Eechaeghau9Yoh9jufiep7H'); | ||
const content = fs.readFileSync('./test/test-1.yaml-crypt'); | ||
const result = yaml.safeLoad(content); | ||
expect(result.key1.toString()).to.equal('Hello, world!'); | ||
require("./crypto-util").setupCrypto(); | ||
describe("yaml-crypt", () => { | ||
it("should load the config file", () => { | ||
const config = loadConfig(); | ||
expect(config).to.not.be.null; | ||
}); | ||
it("should load the config file from the given path", () => { | ||
const configFile = tmp.fileSync(); | ||
fs.writeFileSync(configFile.name, "keys:\n - key: 123"); | ||
const config = loadConfig({ path: configFile.name }); | ||
expect(config).to.not.be.null; | ||
}); | ||
it("should load the config file from home", () => { | ||
const home = tmp.dirSync(); | ||
fs.writeFileSync(`${home.name}/config.yml`, "keys:\n - key: 123"); | ||
const config = loadConfig({ home: home.name }); | ||
expect(config).to.not.be.null; | ||
expect(config.keys).to.have.lengthOf(1); | ||
}); | ||
it("should throw an error when the config file is not readable", () => { | ||
const home = tmp.dirSync(); | ||
fs.mkdirSync(`${home.name}/config.yaml`); | ||
expect(() => loadConfig({ home: home.name })).to.throw(/illegal operation/); | ||
}); | ||
it("should read the decrypted content", () => { | ||
const yaml = yamlcrypt({ keys: "aehae5Ui0Eechaeghau9Yoh9jufiep7H" }); | ||
const content = fs.readFileSync("./test/test-1b.yaml-crypt"); | ||
const result = yaml.decrypt(content); | ||
expect(result.key1.toString()).to.equal("Hello, world!"); | ||
expect(result.key2.toString()).to.equal("Hello, world!"); | ||
}); | ||
it("should read the decrypted raw content (string)", () => { | ||
const yaml = yamlcrypt({ keys: "aehae5Ui0Eechaeghau9Yoh9jufiep7H" }); | ||
const content = fs.readFileSync("./test/test-7.yaml-crypt"); | ||
const result = yaml.decrypt(content); | ||
expect(result).to.equal("Hello!"); | ||
}); | ||
it("should read the decrypted raw content (Buffer)", () => { | ||
const yaml = yamlcrypt({ keys: "aehae5Ui0Eechaeghau9Yoh9jufiep7H" }); | ||
const content = fs.readFileSync("./test/test-7.yaml-crypt"); | ||
const result = yaml.decryptAll(content.toString()); | ||
expect(result[0]).to.equal("Hello!"); | ||
}); | ||
it("should return the encrypted content", () => { | ||
const yaml = yamlcrypt({ | ||
encryptionKey: "aehae5Ui0Eechaeghau9Yoh9jufiep7H" | ||
}); | ||
const str = '{ key1: "Hello, world!", key2: "Hello, world!" }'; | ||
const result = yaml.encrypt(str); | ||
const expected = fs | ||
.readFileSync("./test/test-1a.yaml-crypt") | ||
.toString("utf8"); | ||
expect(result).to.equal(expected); | ||
}); | ||
it('should return the encrypted content', () => { | ||
const yaml = yamlcrypt.encrypt('aehae5Ui0Eechaeghau9Yoh9jufiep7H'); | ||
const result = yaml.safeDump({ | ||
'key1': new yamlcrypt.Plaintext('Hello, world!'), | ||
'key2': new yamlcrypt.Plaintext('Hello, world!', null, 'branca:0xBA') | ||
}); | ||
const expected = fs.readFileSync('./test/test-1.yaml-crypt').toString('utf8'); | ||
expect(result).to.equal(expected); | ||
it("should return the encrypted raw content", () => { | ||
const yaml = yamlcrypt({ | ||
encryptionKey: "aehae5Ui0Eechaeghau9Yoh9jufiep7H" | ||
}); | ||
const expected = fs | ||
.readFileSync("./test/test-7.yaml-crypt") | ||
.toString("utf8"); | ||
const str = "Hello!"; | ||
const result1 = yaml.encrypt(str, { algorithm: "branca", raw: true }); | ||
const result2 = yaml.encryptAll(str, { algorithm: "branca", raw: true }); | ||
expect(result1).to.equal(expected.trim()); | ||
expect(result2).to.equal(expected.trim()); | ||
}); | ||
it('should return the base64 encrypted content', () => { | ||
const yaml = yamlcrypt.encrypt('aehae5Ui0Eechaeghau9Yoh9jufiep7H', { 'base64': true }); | ||
const result = yaml.safeDump({ 'base64': new yamlcrypt.Plaintext('Hello, world!') }); | ||
const expected = fs.readFileSync('./test/test-4.yaml-crypt').toString('utf8'); | ||
expect(result).to.equal(expected); | ||
it("should return the base64 encrypted content", () => { | ||
const yaml = yamlcrypt({ | ||
encryptionKey: "aehae5Ui0Eechaeghau9Yoh9jufiep7H" | ||
}); | ||
const result = yaml.encryptAll("base64: Hello, world!", { base64: true }); | ||
const expected = fs | ||
.readFileSync("./test/test-4.yaml-crypt") | ||
.toString("utf8"); | ||
expect(result).to.equal(expected); | ||
}); | ||
it('should read the decrypted base64 content', () => { | ||
const yaml = yamlcrypt.decrypt('aehae5Ui0Eechaeghau9Yoh9jufiep7H', { 'base64': true }); | ||
const content = fs.readFileSync('./test/test-4.yaml-crypt'); | ||
const result = yaml.safeLoad(content); | ||
expect(result.base64.toString()).to.equal('Hello, world!'); | ||
}); | ||
it("should read the decrypted base64 content", () => { | ||
const yaml = yamlcrypt({ keys: "aehae5Ui0Eechaeghau9Yoh9jufiep7H" }); | ||
const content = fs.readFileSync("./test/test-4.yaml-crypt"); | ||
const result = yaml.decryptAll(content, { base64: true })[0]; | ||
expect(result.base64.toString()).to.equal("Hello, world!"); | ||
}); | ||
it("should correctly transform the nested content", () => { | ||
const yaml = yamlcrypt({ keys: "aehae5Ui0Eechaeghau9Yoh9jufiep7H" }); | ||
const content = fs.readFileSync("./test/test-5.yaml-crypt"); | ||
let decrypted = null; | ||
yaml.transform(content, str => (decrypted = str)); | ||
expect(decrypted).to.contain("str: !<!yaml-crypt/:0> Hello!"); | ||
}); | ||
it("should re-encrypt transformed content", () => { | ||
const yaml = yamlcrypt({ keys: "aehae5Ui0Eechaeghau9Yoh9jufiep7H" }); | ||
const content1 = fs.readFileSync("./test/test-6a.yaml-crypt"); | ||
const content2 = fs.readFileSync("./test/test-6b.yaml-crypt"); | ||
const transformed = yaml.transform(content1, str => | ||
str.replace("Hello!", "Hello, world!") | ||
); | ||
expect(transformed).to.equal(content2.toString()); | ||
}); | ||
it("should encrypt new content when transforming", () => { | ||
const yaml = yamlcrypt({ keys: "aehae5Ui0Eechaeghau9Yoh9jufiep7H" }); | ||
const expected = fs.readFileSync("./test/test-1b.yaml-crypt"); | ||
const newContent = | ||
"key1: !<!yaml-crypt/fernet> Hello, world!\nkey2: !<!yaml-crypt/branca> Hello, world!"; | ||
const transformed = yaml.transform("", () => newContent); | ||
expect(transformed).to.equal(expected.toString()); | ||
}); | ||
it("should not re-encrypt unchanged content when transforming", () => { | ||
const yaml = yamlcrypt({ keys: "aehae5Ui0Eechaeghau9Yoh9jufiep7H" }); | ||
const content = fs.readFileSync("./test/test-7.yaml-crypt"); | ||
const transformed = yaml.transform(content, str => str); | ||
expect(transformed).to.equal(content); | ||
}); | ||
it("should re-encrypt transformed raw content", () => { | ||
const yaml = yamlcrypt({ keys: "aehae5Ui0Eechaeghau9Yoh9jufiep7H" }); | ||
const content = fs.readFileSync("./test/test-7.yaml-crypt"); | ||
const transformed = yaml.transform(content, str => | ||
str.replace("Hello!", "Hello, world!") | ||
); | ||
expect(transformed).to.equal( | ||
"XUvrtHkyXTh1VUW885Ta4V5eQ3hBMFQMC3S3QwEfWzKWVDt3A5TnVUNtVXubi0fsAA8eerahpobwC8" | ||
); | ||
}); | ||
it("should throw an error when an invalid key is given", () => { | ||
expect(() => yamlcrypt({ keys: 0 })).to.throw("invalid key: number"); | ||
}); | ||
it("should throw an error when an empty key is given", () => { | ||
expect(() => yamlcrypt({ keys: "" })).to.throw("empty key!"); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
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
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
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
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
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
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
68140
27
1891
101
6
7
2