Skip to content

Instantly share code, notes, and snippets.

@andrewmackrodt
Last active December 22, 2022 16:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andrewmackrodt/e6a5a2ea7b22d74102ba76becb906566 to your computer and use it in GitHub Desktop.
Save andrewmackrodt/e6a5a2ea7b22d74102ba76becb906566 to your computer and use it in GitHub Desktop.
Decrypt Portable Secret messages using command-line (requires Node.js >= 10)
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