<!DOCTYPE html><!--
Portable Secret generated with https://mprimi.github.io/portable-secret/
This file is self-contained, it embeds an encrypted payload.
It uses your browser's cryptograpy APIs to decrypt it, if you know the password.
--><html><head><metacharset="UTF-8" /><style>body {
background-color: floralwhite;
font-size: large;
margin: 50px;
}
div {
margin: 5px;
}
pre {
padding: 5px;
white-space: pre-wrap;
word-break: keep-all;
}
button {
font-size: large;
padding: 12px20px;
}
input {
font-family: monospace;
}
textarea {
font-family: monospace;
}
.decrypted {
background-color: palegreen;
border: 2px dotted forestgreen;
}
.hint {
background-color: lavender;
border: 2px dashed black;
}
/*
pre.decrypted {
}
*/img.decrypted {
padding: 12px20px;
}
a.decrypted {
font-size: xx-large;
}
input.password_input {
font-size: large;
padding: 12px20px;
}
</style><script>// Display the encryption inputs on the page (invoked during body onload)asyncfunctionloadValues() {
document.getElementById("secret_type").innerHTML = secretType
document.getElementById("salt").setAttribute("value", saltHex)
document.getElementById("iv").setAttribute("value", ivHex)
document.getElementById("cipher").innerHTML = cipherHex
if (secretType == 'file') {
document.getElementById("target_file").innerHTML = `Download file.${secretExt}`
}
}
// Invoked when the 'Decrypt' button is pressedasyncfunctiondecrypt() {
try {
setMessage("Generating key from password...")
// Load salt, convert hex string to byte arraylet salt = hexStringToBytes(saltHex)
if (salt.length != saltSize) {
thrownewError(`Unexpected salt size: ${salt.length}`)
}
// Load IV, convert hex string to byte arraylet iv = hexStringToBytes(ivHex)
if (iv.length != blockSize) {
thrownewError(`Unexpected IV size: ${iv.length}`)
}
// Load password, as byte arraylet password = newTextEncoder().encode(document.getElementById("password").value)
if (password.length == 0) {
thrownewError(`Empty password`)
}
// Wrap password into a Key object, as required by cryptography APIslet passwordKey = awaitwindow.crypto.subtle.importKey(
"raw", // Array of bytes
password,
{name: "PBKDF2"}, // What algorithm uses the keyfalse, // Cannot be extracted
["deriveKey"] // What the key is used for
)
// Derive a key from the password, using PBKDF2let key = awaitwindow.crypto.subtle.deriveKey(
{
name: "PBKDF2", // https://en.wikipedia.org/wiki/PBKDF2salt: salt,
iterations: iterations,
hash: "SHA-1", // As per standard v2.0
},
passwordKey, // Wrapped password
{
name: "AES-GCM", // What algorithm uses the keylength: keySize * 8, // Key bitsize
},
false, // Cannot be extracted
["decrypt"] // What the derived key is used for
)
setMessage("Decrypting...")
// Load ciphertext, convert hex string to byte arraylet cipher = hexStringToBytes(cipherHex)
// Decrypt with AES-GCM// https://en.wikipedia.org/wiki/Galois/Counter_Modelet decryptedBuffer = awaitwindow.crypto.subtle.decrypt(
{
name: "AES-GCM", // Name of block cipher algorithmiv: iv, // Initialization vector
},
key, // Derived key
cipher // Ciphertext
)
// Remove padding (added as necessary for block cipher)// https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS#5_and_PKCS#7
decrypted = removePadding(newUint8Array(decryptedBuffer))
// Render decrypted payload on the pageif (secretType == "message") {
// Decode bytes to UTF-8
plainText = newTextDecoder().decode(decrypted)
// Display the plaintext on the pagedocument.getElementById("target_text").innerHTML = plainText
document.getElementById("text_output_div").hidden = false
} elseif (secretType == "image") {
// Transform image to base64 string
b64Data = btoa(decrypted.reduce((data, byte) => data + String.fromCharCode(byte), ''))
// Create 'data' URI// https://en.wikipedia.org/wiki/Data_URI_schemeconst imageData = `data:image/${secretExt};base64,${b64Data}`// Display image inlinedocument.getElementById("target_image").setAttribute("src", imageData)
document.getElementById("image_output_div").hidden = false
} elseif (secretType == "file") {
// Transform image to base64 string
b64Data = btoa(decrypted.reduce((data, byte) => data + String.fromCharCode(byte), ''))
// Create 'data' URI// https://en.wikipedia.org/wiki/Data_URI_schemeconst fileData = `data:application/octet-stream;base64,${b64Data}`// Activate download linkdocument.getElementById("target_file").setAttribute("href", fileData)
document.getElementById("target_file").setAttribute("download", `file.${secretExt}`)
document.getElementById("file_output_div").hidden = false
} else {
thrownewError(`Unknown secret type: ${secretType}`)
}
setMessage("Decrypted successfully")
} catch (err) {
// TODO better handle failing promisessetMessage(`Decryption failed: ${err}`)
return
}
}
// Transform hexadecimal string to Uint8ArrayfunctionhexStringToBytes(input) {
for (var bytes = [], c = 0; c < input.length; c += 2) {
bytes.push(parseInt(input.substr(c, 2), 16));
}
returnUint8Array.from(bytes);
}
// The cleartext input must be padded to a multiple of the block size// for encryption. This function removes the padding, expected to be// compatible with PKCS#7 described in RFC 5652.// https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS#5_and_PKCS#7functionremovePadding(input) {
// Last byte is the amount of padding
padAmount = input[input.length-1]
unpaddedSize = input.length - padAmount
return input.slice(0, unpaddedSize)
}
// Update page with status of decryptionfunctionsetMessage(msg) {
document.getElementById("errormsg").innerHTML = msg
}
</script></head><bodyonload="loadValues()"><h1>This page contains a secret <spanid="secret_type"></span></h1><h2>Enter the password to decrypt it</h2><h3>Created with <ahref="https://mprimi.github.io/portable-secret/">Portable Secret</a></h3><p>
This file contains a secret (message, file, or image) that can be recovered if you know the password.<br>
The secret can be decrypted without an internet connection, this file has no dependencies and no data leaves the browser window.
</p><div><h4>Password hint:</h4><preclass="hint">A yellow elongated fruit (technically a berry!)
6 letters, all lowercase.</pre></div><div><h4>Password:</h4><inputtype="text"id="password"placeholder="See hint above"class="password_input"required></div><div><buttontype="button"onclick='decrypt()'>Decrypt</button><spanid="errormsg"></span></div><divid="text_output_div"hidden><preid="target_text"class="decrypted"></pre></div><divid="image_output_div"hidden><imgid="target_image"class="decrypted"></div><divid="file_output_div"hidden><aid="target_file"class="decrypted">Download</a></div><details><summary>Details</summary>
These are decryption inputs, that can be safely transmitted in the clear.
Without the correct password, they are useless.
<div>
Salt:
<inputtype="text"id="salt"value=""readonly></div><div>
IV:
<inputtype="text"id="iv"value=""readonly></div><div>
Ciphertext:<br><textarearows="8"cols="80"id="cipher"readonly></textarea></div></details></body><script>const secretType = "message"const secretExt = ""const saltSize = 16// bytesconst blockSize = 16// bytesconst keySize = 32// bytesconst iterations = 1000000const saltHex = "cb70a6cbf6fcbccf9c079aa432729296"const ivHex = "d94c66c3806435e13c0e06a8d2aae847"const cipherHex = "6dd82d671ca8aae6715cecbd32f7cb557bf92cc06b5f78d8cb060799e98beed4"</script></html>