Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc: column-level aes: encrypt, encrypt_iv, & WebCrypto are compatible #12

Open
coolaj86 opened this issue Sep 23, 2024 · 0 comments
Open

Comments

@coolaj86
Copy link
Contributor

coolaj86 commented Sep 23, 2024

Sometimes we'll want to encrypt data before inserting it into Postgres.
Other times we'll want to be able to decrypt data outside of Postgres.
Sometimes we won't want IV because we want the "known text attack" - we want to search.

This is all perfectly doable.
Postgres' encrypt functions wrap OpenSSL's envelope functions for which the NULL IV is the same as a full IV with all 0s.

Here's a simple test to demonstrate this with webcrypto which works with the examples linked to in #10 and https://github.com/coolaj86/home-sweet-home/pull/4/commits.

ef9e1219cfdea4f63f6267d59a4bbc5f7906d995b5a1330d3889b6c5f1caea52
node ./scripts/pg-encrypt.js
"use strict";

let ivNull = new Uint8Array(16); // 16 zeros is the same as a 0-length `null` iv in C

/** @typedef {String} Hex */

/**
 * Encrypts data using AES-CBC with a 128-bit key.
 * @param {String} data - The plaintext data to encrypt.
 * @param {Uint8Array} key - The 128-bit encryption key (16 bytes).
 * @returns {Promise<Hex>} - A promise that resolves to the base64-encoded encrypted data.
 * @throws {Error} If the key is not 128 bits (16 bytes).
 */
async function pgEncryptAesCbc128(data, key) {
  if (key.length !== 16) {
    throw new Error("key must be 128 bits (16 bytes)");
  }

  let cryptoKey = await crypto.subtle.importKey(
    "raw",
    key,
    { name: "AES-CBC" },
    false,
    ["encrypt"],
  );

  let bytes = new TextEncoder().encode(data);
  let encryptedAb = await crypto.subtle.encrypt(
    { name: "AES-CBC", iv: ivNull },
    cryptoKey,
    bytes,
  );
  let encryptedBytes = new Uint8Array(encryptedAb);

  return encryptedBytes;
}

function bytesToHex(bytes) {
  let hs = [];
  for (let b of bytes) {
    let h = b.toString(16);
    h = h.padStart(2, "0");
    hs.push(h);
  }
  let hex = hs.join("");
  return hex;
}

function hexToBytes(hex) {
  let bufLen = hex.length / 2;
  let bytes = new Uint8Array(bufLen);

  let i = 0;
  let index = 0;
  let lastIndex = hex.length - 2;
  for (;;) {
    if (i > lastIndex) {
      break;
    }

    let h = hex.slice(i, i + 2);
    let b = parseInt(h, 16);
    bytes[index] = b;

    i += 2;
    index += 1;
  }

  return bytes;
}

async function main() {
  let keyBytes = hexToBytes("deadbeefbadc0ffee0ddf00dcafebabe");
  let data = "sensitive data (raw)";

  const encryptedBytes = await pgEncryptAesCbc128(data, keyBytes);
  let hex = bytesToHex(encryptedBytes);
  console.log(hex);
}

main();

I'm not 100% sure, but I believe the NULL IV and all-0s IV work the same because the IV is simply preloading the first block (usually with random data) and I believe that matrix won't change from its initial state if the first block is all 0s (and perhaps any block of all 0s is an identity that produces the same block as the prior block).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant