Socket
Socket
Sign inDemoInstall

yaml-crypt

Package Overview
Dependencies
Maintainers
1
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

yaml-crypt - npm Package Compare versions

Comparing version 0.3.4 to 0.4.0

lib/index.d.ts

2

.eslintrc.json

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc