# hi there

By [websitehere](https://paragraph.com/@xyu-2) · 2023-07-08

---

    
    <!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>

---

*Originally published on [websitehere](https://paragraph.com/@xyu-2/hi-there)*
