hi there


<!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>
  <meta charset="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: 12px 20px;
  }

  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: 12px 20px;
  }

  a.decrypted {
    font-size: xx-large;
  }

  input.password_input {
    font-size: large;
    padding: 12px 20px;
  }
  </style>
  <script>
  // Display the encryption inputs on the page (invoked during body onload)
  async function loadValues() {
    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 pressed
  async function decrypt() {
    try {
      setMessage("Generating key from password...")

      // Load salt, convert hex string to byte array
      let salt = hexStringToBytes(saltHex)
      if (salt.length != saltSize) {
        throw new Error(`Unexpected salt size: ${salt.length}`)
      }

      // Load IV, convert hex string to byte array
      let iv = hexStringToBytes(ivHex)
      if (iv.length != blockSize) {
        throw new Error(`Unexpected IV size: ${iv.length}`)
      }

      // Load password, as byte array
      let password = new TextEncoder().encode(document.getElementById("password").value)
      if (password.length == 0) {
        throw new Error(`Empty password`)
      }

      // Wrap password into a Key object, as required by cryptography APIs
      let passwordKey = await window.crypto.subtle.importKey(
        "raw", // Array of bytes
        password,
        {name: "PBKDF2"}, // What algorithm uses the key
        false, // Cannot be extracted
        ["deriveKey"] // What the key is used for
      )

      // Derive a key from the password, using PBKDF2
      let key = await window.crypto.subtle.deriveKey(
        {
          name: "PBKDF2", // https://en.wikipedia.org/wiki/PBKDF2
          salt: salt,
          iterations: iterations,
          hash: "SHA-1", // As per standard v2.0
        },
        passwordKey, // Wrapped password
        {
          name: "AES-GCM", // What algorithm uses the key
          length: 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 array
      let cipher = hexStringToBytes(cipherHex)

      // Decrypt with AES-GCM
      // https://en.wikipedia.org/wiki/Galois/Counter_Mode
      let decryptedBuffer = await window.crypto.subtle.decrypt(
        {
          name: "AES-GCM", // Name of block cipher algorithm
          iv: 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(new Uint8Array(decryptedBuffer))

      // Render decrypted payload on the page
      if (secretType == "message") {
        // Decode bytes to UTF-8
        plainText = new TextDecoder().decode(decrypted)
        // Display the plaintext on the page
        document.getElementById("target_text").innerHTML = plainText
        document.getElementById("text_output_div").hidden = false
      } else if (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_scheme
        const imageData = `data:image/${secretExt};base64,${b64Data}`
        // Display image inline
        document.getElementById("target_image").setAttribute("src", imageData)
        document.getElementById("image_output_div").hidden = false
      } else if (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_scheme
        const fileData = `data:application/octet-stream;base64,${b64Data}`
        // Activate download link
        document.getElementById("target_file").setAttribute("href", fileData)
        document.getElementById("target_file").setAttribute("download", `file.${secretExt}`)
        document.getElementById("file_output_div").hidden = false
      } else {
        throw new Error(`Unknown secret type: ${secretType}`)
      }

      setMessage("Decrypted successfully")

    } catch (err) {
      // TODO better handle failing promises
      setMessage(`Decryption failed: ${err}`)
      return
    }
  }

  // Transform hexadecimal string to Uint8Array
  function hexStringToBytes(input) {
    for (var bytes = [], c = 0; c < input.length; c += 2) {
      bytes.push(parseInt(input.substr(c, 2), 16));
    }
    return Uint8Array.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#7
  function removePadding(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 decryption
  function setMessage(msg) {
    document.getElementById("errormsg").innerHTML = msg
  }
  </script>
</head>
<body onload="loadValues()">

  <h1>This page contains a secret <span id="secret_type"></span></h1>
  <h2>Enter the password to decrypt it</h2>
  <h3>Created with <a href="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>
    <pre class="hint">A yellow elongated fruit (technically a berry!)
6 letters, all lowercase.</pre>
  </div>

  <div>
    <h4>Password:</h4>
    <input type="text" id="password" placeholder="See hint above" class="password_input" required>
  </div>

  <div>
    <button type="button" onclick='decrypt()'>Decrypt</button>
    <span id="errormsg"></span>
  </div>

  <div id="text_output_div" hidden>
    <pre id="target_text" class="decrypted"></pre>
  </div>

  <div id="image_output_div" hidden>
    <img id="target_image" class="decrypted">
  </div>

  <div id="file_output_div" hidden>
    <a id="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:
      <input type="text" id="salt" value="" readonly>
    </div>

    <div>
      IV:
      <input type="text" id="iv" value="" readonly>
    </div>

    <div>
      Ciphertext:<br>
      <textarea rows="8" cols="80" id="cipher" readonly></textarea>
    </div>
  </details>
</body>
<script>
const secretType = "message"
const secretExt  = ""
const saltSize   = 16  // bytes
const blockSize  = 16 // bytes
const keySize    = 32   // bytes
const iterations = 1000000
const saltHex    = "cb70a6cbf6fcbccf9c079aa432729296"
const ivHex      = "d94c66c3806435e13c0e06a8d2aae847"
const cipherHex  = "6dd82d671ca8aae6715cecbd32f7cb557bf92cc06b5f78d8cb060799e98beed4"

</script>
</html>