Last active
December 22, 2022 16:51
-
-
Save andrewmackrodt/e6a5a2ea7b22d74102ba76becb906566 to your computer and use it in GitHub Desktop.
Decrypt Portable Secret messages using command-line (requires Node.js >= 10)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const crypto = require('crypto') | |
const fs = require('fs') | |
const readline = require('readline') | |
const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) | |
const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)) | |
void (async () => { | |
const args = process.argv.slice(2) | |
if (args.length === 0) { | |
process.stderr.write('Usage: node portable-secret-decrypt.js encrypted-file.html\n') | |
process.exit(2) | |
} | |
const inputFile = args[0] | |
if (!fs.existsSync(inputFile)) { | |
process.stderr.write(`ERROR file not found: ${inputFile}\n`) | |
process.exit(4) | |
} | |
let inputText = fs.readFileSync(inputFile).toString() | |
const getInputNumber = (key) => { | |
const match = new RegExp(`\\b${key}[ \t]*=[ \t]*([0-9]+)`).exec(inputText) | |
if (!match) { | |
throw new Error(`Failed to parse input param: ${key}`) | |
} | |
const value = match[1] | |
if (process.env.DEBUG) { | |
process.stderr.write(`${key}: ${value}\n`) | |
} | |
return parseInt(value, 10) | |
} | |
const getInputString = (key, defaultValue) => { | |
const match = new RegExp(`\\b${key}[ \t]*=[ \t]*(?:'([^']+)'|"([^"]+)")`, 'i').exec(inputText) | |
let value | |
if (!match) { | |
if (typeof defaultValue !== 'string') { | |
throw new Error(`Failed to parse input param: ${key}`) | |
} | |
value = defaultValue | |
} else { | |
value = match[1] || match[2] | |
} | |
if (process.env.DEBUG) { | |
process.stderr.write(`${key}: ${value}\n`) | |
} | |
return value | |
} | |
// parse all params from html file | |
const secretType = getInputString('secretType', 'file') | |
const secretExt = getInputString('secretExt', 'txt') | |
const keySize = getInputNumber('keySize') | |
const iterations = getInputNumber('iterations') | |
const saltHex = getInputString('saltHex') | |
const ivHex = getInputString('ivHex') | |
const cipherHex = getInputString('cipherHex') | |
delete inputText | |
const filename = `decrypted.${secretExt}` | |
if (secretType !== 'message') { | |
if (fs.existsSync(filename)) { | |
process.stderr.write(`ERROR failed to decrypt: EEXIST: file already exists, open '${filename}'\n`) | |
process.exit(8) | |
} | |
} | |
// prompt for password | |
const password = await prompt('Enter decryption password: ') | |
// construct decryptor | |
const cipher = Buffer.from(cipherHex, 'hex') | |
const salt = Buffer.from(saltHex, 'hex') | |
const key = crypto.pbkdf2Sync(password, salt, iterations, keySize, 'SHA1') | |
const iv = Buffer.from(ivHex, 'hex') | |
const decipher = crypto.createDecipheriv('AES-256-GCM', key, iv) | |
decipher.setAuthTag(cipher.slice(-16)) | |
try { | |
// decrypt contents | |
const output = Buffer.concat([ | |
decipher.update(cipher.slice(0, -16)), | |
decipher.final() | |
]) | |
// determine padding to trim | |
const padding = output[output.length - 1] | |
const size = output.length - padding | |
const decrypted = output.slice(0, size) | |
if (secretType === 'message') { | |
// print decrypted contents to stdout | |
process.stdout.write(decrypted) | |
} else { | |
// write decrypted contents to file only if not exists | |
fs.writeFileSync(filename, decrypted, { flag: 'wx' }) | |
process.stdout.write(`SUCCESS! Created file: '${filename}'\n`) | |
} | |
} catch (e) { | |
process.stderr.write(`ERROR failed to decrypt: ${e.toString().replace(/\bError:? ?/i, '')}\n`) | |
process.exit(16) | |
} finally { | |
rl.close() | |
} | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment