]> git.ardis.lu - static-encrypt/commitdiff
Initial commit
authorArdis Lu <ardisglu@gmail.com>
Mon, 20 Feb 2023 00:52:11 +0000 (16:52 -0800)
committerArdis Lu <ardisglu@gmail.com>
Mon, 20 Feb 2023 00:52:11 +0000 (16:52 -0800)
README.md [new file with mode: 0644]
index.html [new file with mode: 0644]
template.html [new file with mode: 0644]

diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..bd92e41
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# static-encrypt
+
+A tool to make a password-protected frontend that can be hosted statically.
+
+Like [staticrypt](https://github.com/robinmoisson/staticrypt) but uses the modern [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) instead of `crypto-js`.
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..2f01352
--- /dev/null
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <link rel="icon" href="data:,">
+  <title>static-encrypt</title>
+  <style>
+    * {
+      margin: 0;
+      box-sizing: border-box;
+    }
+
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
+      display: grid;
+      place-items: center;
+    }
+
+    form {
+      inline-size: min(100dvi - 16px, 720px);
+      display: flex;
+      flex-direction: column;
+      gap: 4px;
+    }
+
+    label {
+      font-weight: 500;
+    }
+
+    textarea,
+    input,
+    button {
+      inline-size: 100%;
+      font-family: inherit;
+      font-size: 1rem;
+      padding: 6px 4px;
+      margin-block-end: 16px;
+    }
+
+    textarea {
+      block-size: 20rem;
+    }
+
+    button {
+      inline-size: fit-content;
+      padding: 8px;
+      border-radius: 4px;
+      border: 1px solid grey;
+    }
+  </style>
+</head>
+
+<body>
+  <main>
+    <form>
+      <label for="content">Content</label>
+      <textarea name="content" id="content" required></textarea>
+
+      <label for="password">Password</label>
+      <input type="password" name="password" id="password" required>
+
+      <button name="encrypt">Encrypt</button>
+
+      <output name="output"></output>
+    </form>
+  </main>
+
+  <script type="module">
+    async function encrypt(plaintext, password) {
+      const encoder = new TextEncoder();
+      const encodedPlaintext = encoder.encode(plaintext);
+      const encodedPassword = encoder.encode(password);
+
+      const salt = new Uint8Array(32);
+      const iv = new Uint8Array(32);
+      crypto.getRandomValues(salt);
+      crypto.getRandomValues(iv);
+
+      const keyMaterial = await crypto.subtle.importKey(
+        'raw',
+        encodedPassword,
+        'PBKDF2',
+        false,
+        ['deriveKey'],
+      );
+
+      const key = await crypto.subtle.deriveKey(
+        {
+          name: 'PBKDF2',
+          hash: 'SHA-512',
+          salt,
+          iterations: 400_000,
+        },
+        keyMaterial,
+        { 'name': 'AES-GCM', 'length': 256 },
+        false,
+        ['encrypt']
+      );
+
+      const ciphertext = new Uint8Array(await crypto.subtle.encrypt(
+        { name: 'AES-GCM', iv },
+        key,
+        encodedPlaintext,
+      ));
+
+      const contentBuffer = new Uint8Array(salt.byteLength + iv.byteLength + ciphertext.byteLength);
+      contentBuffer.set(salt);
+      contentBuffer.set(iv, salt.byteLength);
+      contentBuffer.set(ciphertext, salt.byteLength + iv.byteLength);
+      const content = btoa(String.fromCharCode(...contentBuffer));
+
+      return content;
+    }
+
+    const template = await fetch('/template.html').then(r => r.text());
+    document.querySelector('form').addEventListener('submit', async e => {
+      e.preventDefault();
+
+      const data = new FormData(e.target);
+      const content = data.get('content');
+      const password = data.get('password');
+
+      const encrypted = await encrypt(content, password);
+      const output = template.replace("const content = 'DO_NOT_MODIFY_THIS_LINE';", `const content = '${encrypted}';`);
+      e.target.elements.output.textContent = output;
+    });
+  </script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/template.html b/template.html
new file mode 100644 (file)
index 0000000..61d2787
--- /dev/null
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <link rel="icon" href="data:,">
+  <title>Enter password</title>
+  <style>
+    body {
+      min-block-size: 100dvb;
+      margin: 0;
+      display: grid;
+      place-items: center;
+    }
+  </style>
+</head>
+
+<body>
+  <main>
+    <form>
+      <input type="password" name="password" id="password" required>
+      <button name="decrypt">Decrypt</button>
+    </form>
+  </main>
+
+  <script>
+    const content = 'DO_NOT_MODIFY_THIS_LINE';
+
+    async function decrypt(content, password) {
+      const encoder = new TextEncoder();
+      const encodedPassword = encoder.encode(password);
+
+      const contentBuffer = Uint8Array.from(atob(content), c => c.charCodeAt(0));
+      const salt = new Uint8Array(32);
+      const iv = new Uint8Array(32);
+      const ciphertext = new Uint8Array(contentBuffer.byteLength - 64);
+      salt.set(contentBuffer.slice(0, 32));
+      iv.set(contentBuffer.slice(32, 64));
+      ciphertext.set(contentBuffer.slice(64));
+
+      const keyMaterial = await crypto.subtle.importKey(
+        'raw',
+        encodedPassword,
+        'PBKDF2',
+        false,
+        ['deriveKey'],
+      );
+
+      const key = await crypto.subtle.deriveKey(
+        {
+          name: 'PBKDF2',
+          hash: 'SHA-512',
+          salt,
+          iterations: 400_000,
+        },
+        keyMaterial,
+        { 'name': 'AES-GCM', 'length': 256 },
+        false,
+        ['decrypt']
+      );
+
+      const encodedPlaintext = new Uint8Array(await crypto.subtle.decrypt(
+        { name: 'AES-GCM', iv },
+        key,
+        ciphertext,
+      ));
+
+      const plaintext = new TextDecoder().decode(encodedPlaintext);
+
+      return plaintext;
+    }
+
+    document.querySelector('form').addEventListener('submit', async e => {
+      e.preventDefault();
+      
+      const data = new FormData(e.target);
+      const password = data.get('password');
+
+      document.querySelector('html').innerHTML = await decrypt(content, password);
+    });
+  </script>
+</body>
+
+</html>
\ No newline at end of file
This page took 0.051478 seconds and 4 git commands to generate.