UNPKG

hidr

Version:

A CLI tool for securely sharing secrets

489 lines (403 loc) 12.3 kB
#!/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;