hidr
Version:
A CLI tool for securely sharing secrets
489 lines (403 loc) • 12.3 kB
JavaScript
#!/usr/bin/env node
'use strict';
var require$$0$1 = require('crypto');
var require$$0 = require('fs');
var require$$2$1 = require('commander');
var require$$3$1 = require('ms');
var require$$1 = require('path');
var require$$2 = require('os');
var require$$3 = require('isbinaryfile');
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var src = {};
var name = "hidr";
var version = "1.2.2";
var require$$4 = {
name: name,
version: version};
var file;
var hasRequiredFile;
function requireFile () {
if (hasRequiredFile) return file;
hasRequiredFile = 1;
const fs = require$$0;
const path = require$$1;
const os = require$$2;
const isBinaryFile = require$$3.isBinaryFileSync;
const packageJson = require$$4;
function readFileContent(filePath) {
if (!fs.existsSync(filePath)) {
console.error(`Error: File "${filePath}" does not exist.`);
process.exit(1);
}
const isBinary = isBinaryFile(filePath);
if (isBinary) {
console.error(`Error: File "${filePath}" is not a valid text file.`);
process.exit(1);
}
// Check if file is larger than 100KB
const fileSize = fs.statSync(filePath).size;
const maxSize = 100 * 1024; // 100KB in bytes
if (fileSize > maxSize) {
console.error(
`Error: File "${filePath}" is too large. Maximum size is 100KB.`
);
process.exit(1);
}
return fs.readFileSync(filePath, "utf8");
}
function getAppDataDir() {
const homeDir = os.homedir();
const appDir = path.join(homeDir, `.${packageJson.name}`);
return appDir;
}
function createAppDirIfNotExists() {
const appDir = getAppDataDir();
if (!fs.existsSync(appDir)) {
fs.mkdirSync(appDir, { recursive: true, mode: 0o700 });
}
}
function getUserId() {
const configPath = path.join(getAppDataDir(), "config.json");
if (!fs.existsSync(configPath)) {
return null;
}
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
return config.userId;
}
function saveUserId(userId) {
createAppDirIfNotExists();
const configPath = path.join(getAppDataDir(), "config.json");
fs.writeFileSync(configPath, JSON.stringify({ userId, createdAt: Date.now() }), {
mode: 0o600,
});
}
function savePrivateKey(privateKey) {
createAppDirIfNotExists();
const keyPath = path.join(getAppDataDir(), "key.pem");
fs.writeFileSync(keyPath, privateKey, {
mode: 0o600,
});
}
function getPrivateKey() {
const keyPath = path.join(getAppDataDir(), "key.pem");
if (!fs.existsSync(keyPath)) {
return null;
}
return fs.readFileSync(keyPath, "utf8");
}
file = {
readFileContent,
getAppDataDir,
getPrivateKey,
savePrivateKey,
getUserId,
saveUserId,
};
return file;
}
var encryption;
var hasRequiredEncryption;
function requireEncryption () {
if (hasRequiredEncryption) return encryption;
hasRequiredEncryption = 1;
const crypto = require$$0$1;
const ALGORITHM = "aes-256-gcm";
function encrypt({ content, publicKey }) {
const encryptionKey = crypto.randomBytes(16);
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv(
ALGORITHM,
Buffer.concat([encryptionKey, encryptionKey]),
iv
);
let encryptedContent = cipher.update(content, "utf8", "hex");
encryptedContent += cipher.final("hex");
const authTag = cipher.getAuthTag().toString("hex");
const payload = {
iv: iv.toString("hex"),
content: encryptedContent,
tag: authTag,
key: encryptionKey.toString("hex"),
};
if (publicKey) {
const encryptedKey = crypto
.publicEncrypt(publicKey, encryptionKey)
.toString("hex");
payload.key = encryptedKey;
}
return payload;
}
function decrypt(payload) {
const { iv, content, tag, key } = payload;
const encryptionKey = Buffer.from(key, "hex");
const fullKey = Buffer.concat([encryptionKey, encryptionKey]);
const decipher = crypto.createDecipheriv(
ALGORITHM,
fullKey,
Buffer.from(iv, "hex")
);
decipher.setAuthTag(Buffer.from(tag, "hex"));
let decryptedContent = decipher.update(content, "hex", "utf8");
decryptedContent += decipher.final("utf8");
return decryptedContent;
}
function generateKeyPair() {
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
publicKeyEncoding: {
type: "spki",
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
});
return { publicKey, privateKey };
}
function rsaDecrypt(privateKey, content) {
try {
const decrypted = crypto.privateDecrypt(
privateKey,
Buffer.from(content, "hex")
);
return decrypted.toString("hex");
} catch (error) {
throw new Error("Failed to view secret");
}
}
function rsaSign(privateKey, content) {
const signer = crypto.createSign("RSA-SHA256");
signer.update(content);
signer.end();
return signer.sign(privateKey, "base64");
}
encryption = {
encrypt,
decrypt,
generateKeyPair,
rsaDecrypt,
rsaSign,
};
return encryption;
}
var hasRequiredSrc;
function requireSrc () {
if (hasRequiredSrc) return src;
hasRequiredSrc = 1;
const crypto = require$$0$1;
const fs = require$$0;
const { Command } = require$$2$1;
const ms = require$$3$1;
const packageJson = require$$4;
const {
readFileContent,
getUserId,
savePrivateKey,
saveUserId,
getPrivateKey,
} = requireFile();
const {
encrypt,
decrypt,
generateKeyPair,
rsaDecrypt,
rsaSign,
} = requireEncryption();
const API_BASE_URL = "https://secrets-backend.msdcconnect.workers.dev";
const program = new Command();
program
.name("secret-cli")
.description("A CLI tool for securely sharing secrets")
.version(packageJson.version);
program
.command("init")
.argument('<user-id>', 'A unique identifier for this machine')
.description("Generate keys to receive secrets only readable on this machine")
.action(async (userId) => {
try {
const existingUserId = getUserId();
if (existingUserId) {
console.log(`User already initialized with ID: ${existingUserId}`);
process.exit(1);
}
const { publicKey, privateKey } = generateKeyPair();
const response = await fetch(`${API_BASE_URL}/init`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId, publicKey }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error);
}
saveUserId(userId);
savePrivateKey(privateKey);
console.log(`Successfully initialized ${userId}`);
} catch (error) {
console.error(`Failed to initialize ${userId}`, error.message);
process.exit(1);
}
});
program
.command("share [secret]")
.description("Share a secret or a text file")
.option("-f, --file <path>", "Path to a text file containing the secret")
.option("-t, --ttl <ttl>", "Time-to-live (e.g. 1m, 2h, 1d)")
.option("-u, --uid <user-id>", "Secret can only be read by this user")
.option(
"-l, --limit <count>",
"Number of times the secret can be read",
parseInt
)
.action(async (secret, options) => {
if (!secret && !options.file) {
console.error("Error: Provide a secret text or a file.");
process.exit(1);
}
const DEFAULT_TTL = 60 * 60 * 24 * 7; // 7 days
let ttl = DEFAULT_TTL;
if (options.ttl) {
const milliseconds = ms(options.ttl);
if (!milliseconds) {
console.error(
"Error: Invalid TTL. Please provide a valid time duration (e.g. 1m, 2h, 1d)"
);
process.exit(1);
}
ttl = milliseconds / 1000;
if (ttl < 60) {
console.error("Error: Invalid TTL. TTL must be at least 1 minute");
process.exit(1);
}
}
let content = secret;
if (options.file) {
content = readFileContent(options.file);
}
const data = {
content,
};
if (options.uid) {
const publicKeyResponse = await fetch(
`${API_BASE_URL}/users/${options.uid}/key`
);
if (!publicKeyResponse.ok) {
console.error("Error: Invalid user id provided");
process.exit(1);
}
const { publicKey } = await publicKeyResponse.json();
data.publicKey = publicKey;
}
try {
const encrypted = encrypt(data);
const id = crypto.randomBytes(8).toString("hex");
const payload = {
id,
content: encrypted.content,
iv: encrypted.iv,
tag: encrypted.tag,
...(options.limit && { reads: options.limit }),
...(options.ttl && { ttl }),
...(options.uid && {
uid: options.uid,
encrypted: encrypted.key,
}),
};
const response = await fetch(`${API_BASE_URL}/store`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error);
}
let secretId = options.uid ? id : encrypted.key + id;
secretId = Buffer.from(secretId, "hex").toString("base64url");
console.log("\nTo view this secret, run:");
console.log(`npx hidr view ${secretId}`);
} catch (error) {
console.error(`Failed to share secret: ${error.message}`);
process.exit(1);
}
});
program
.command("view <secret-id>")
.description("Retrieve a shared secret")
.option("-o, --output <file>", "Save output to a file")
.action(async (secretId, options) => {
try {
const buffer = Buffer.from(secretId, "base64url");
const hexString = buffer.toString("hex");
let id = "";
let key = "";
if (hexString.length <= 16) {
id = hexString;
} else {
key = hexString.slice(0, 32);
id = hexString.slice(32);
}
const response = await fetch(`${API_BASE_URL}/retrieve/${id}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error);
}
const data = await response.json();
const decryptPayload = {
content: data.content,
iv: data.iv,
tag: data.tag,
key: key,
};
let privateKey = null;
if (data.encrypted) {
privateKey = getPrivateKey();
if (!privateKey) {
console.error(
"Error: You do not have permission to view this secret"
);
process.exit(1);
}
const decryptedKey = rsaDecrypt(privateKey, data.encrypted);
decryptPayload.key = decryptedKey;
}
const decryptedContent = decrypt(decryptPayload);
if (options.output) {
fs.writeFileSync(options.output, decryptedContent);
console.log(`Successfully saved secret to: ${options.output}`);
} else {
console.log(decryptedContent);
}
if (data.remainingReads !== null) {
if (data.encrypted) {
const signature = rsaSign(privateKey, id);
const updateResponse = await fetch(`${API_BASE_URL}/reads/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-Hidr-Signature": signature,
},
});
if(updateResponse.ok) {
console.log(`\nRemaining reads: ${data.remainingReads - 1}`);
}
} else {
console.log(`\nRemaining reads: ${data.remainingReads}`);
}
}
} catch (error) {
console.error("Failed to retrieve secret:", error.message);
process.exit(1);
}
});
program.parse(process.argv);
return src;
}
var srcExports = requireSrc();
var index = /*@__PURE__*/getDefaultExportFromCjs(srcExports);
module.exports = index;