Skip to content

2. Bonus Tutorials : Introduction to Cryptography

Bernard Sibanda edited this page Jun 6, 2025 · 1 revision

Table of Contents

  1. SHA-256 (Secure Hash Algorithm 256-bit)
  2. PBKDF2 (Password-Based Key Derivation Function 2)
  3. AES-GCM (Advanced Encryption Standard in Galois/Counter Mode)
  4. ed25519 (Edwards-curve Digital Signature Algorithm)
  5. BLAKE2b (Blake2-b Hash)
  6. Bech32 (Human-Readable Address Encoding)
  7. CIP-1852 HD Key Derivation (Extended ed25519 Keys)
  8. Putting It All Together: Cardano-Flavored Secure Notes
  9. Glossary of Terms

1. SHA-256 (Secure Hash Algorithm 256-bit)

What it is

SHA-256 is a one-way cryptographic hash function. You feed it any input (text, binary, etc.), and it outputs a fixed-length 256-bit (32-byte) digest. Even the slightest change in input produces a completely different hash. It’s infeasible to reverse the hash (i.e., you can’t recover the original message from its SHA-256).

Why it’s useful

  • Data integrity: Verify that files/messages haven’t been tampered with.
  • Fingerprinting: Produce a “fingerprint” for arbitrary data without revealing the data itself.
  • Address hashing in Cardano: Cardano uses a variation of Blake2b for address payloads, but SHA-256 is a common reference point and used in other contexts (e.g., hashing metadata).

How to use it

async function sha256Hex(message) {
  // 1) Convert the string to bytes
  const msgBuffer = new TextEncoder().encode(message);

  // 2) Digest using SHA-256
  const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);

  // 3) Convert ArrayBuffer to hex string
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
  return hashHex;
}

// ── Example Usage ──
(async () => {
  const text = "Hello, Cardano!";
  const digest = await sha256Hex(text);
  console.log(`SHA-256("${text}") = ${digest}`);
  // Output (sample):
  // SHA-256("Hello, Cardano!") = 3a7bd3e2360a... (64 hex characters)
})();

2. PBKDF2 (Password-Based Key Derivation Function 2)

What it is

PBKDF2 turns a user-supplied password and a random salt into a cryptographic key suitable for AES or HMAC. It repeatedly hashes the password+salt thousands (or hundreds of thousands) of times, making brute-force attacks much more expensive.

Why it’s useful

  • Secure key derivation: Don’t use raw passwords to encrypt data—use PBKDF2 to “stretch” the password into a strong key.
  • Salting: Ensure two users with the same password still produce different keys (because of different salts).

How to use it

// 1) Derive an AES-GCM key from a password + Base64 salt
async function deriveAesKeyFromPassword(password, saltB64) {
  // a) Convert password to Uint8Array
  const pwBytes = new TextEncoder().encode(password);

  // b) Import as a “PBKDF2” key
  const baseKey = await crypto.subtle.importKey(
    "raw",
    pwBytes,
    { name: "PBKDF2" },
    false,
    ["deriveKey"]
  );

  // c) Convert salt from Base64 to Uint8Array
  const saltBytes = Uint8Array.from(atob(saltB64), c => c.charCodeAt(0));

  // d) Derive an AES-GCM 256-bit key (100 000 iterations, SHA-256)
  return crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt: saltBytes,
      iterations: 100_000,
      hash: "SHA-256"
    },
    baseKey,
    { name: "AES-GCM", length: 256 },
    true,            // extractable (so you could export it if needed)
    ["encrypt", "decrypt"]
  );
}

// 2) Generate a random 16-byte salt (Base64)
function randomSaltBase64() {
  const salt = crypto.getRandomValues(new Uint8Array(16));
  return btoa(String.fromCharCode(...salt));
}

// ── Example Usage ──
(async () => {
  const password = "S3curePass!";
  const saltB64  = randomSaltBase64();
  console.log("Salt (Base64):", saltB64);

  const aesKey = await deriveAesKeyFromPassword(password, saltB64);
  console.log("Derived AES-GCM key:", aesKey);
})();

3. AES-GCM (Advanced Encryption Standard in Galois/Counter Mode)

What it is

AES-GCM is a symmetric encryption algorithm that provides both confidentiality (encrypts the plaintext) and integrity/authenticity (any tampering with the ciphertext or IV will cause decryption to fail).

  • Key: 256-bit (32 bytes) symmetric key.
  • IV (nonce): 12 bytes random, must be unique per encryption.

Why it’s useful

  • It’s widely supported in browsers via Web Crypto (crypto.subtle.encrypt / decrypt).
  • Fast and secure—used extensively on Cardano for encrypting wallet data or off-chain data at rest.

How to encrypt + decrypt

// A) Encrypt a UTF-8 string under AES-GCM
async function aesGcmEncrypt(plaintext, cryptoKey) {
  // 1) Convert plaintext to bytes
  const ptBytes = new TextEncoder().encode(plaintext);

  // 2) Generate a random 12-byte IV
  const iv = crypto.getRandomValues(new Uint8Array(12));

  // 3) Perform AES-GCM encryption
  const ctBuffer = await crypto.subtle.encrypt(
    {
      name: "AES-GCM",
      iv: iv
    },
    cryptoKey,
    ptBytes
  );

  // 4) Return Base64-encoded IV + ciphertext
  const ivB64 = btoa(String.fromCharCode(...iv));
  const ctB64 = btoa(String.fromCharCode(...new Uint8Array(ctBuffer)));
  return { iv: ivB64, ciphertext: ctB64 };
}

// B) Decrypt an AES-GCM ciphertext given Base64 IV + ciphertext
async function aesGcmDecrypt(ivB64, ctB64, cryptoKey) {
  // 1) Convert Base64 IV + ciphertext back to Uint8Array
  const ivBytes = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0));
  const ctBytes = Uint8Array.from(atob(ctB64), c => c.charCodeAt(0));

  // 2) Ask Web Crypto to decrypt
  const plainBuffer = await crypto.subtle.decrypt(
    {
      name: "AES-GCM",
      iv: ivBytes
    },
    cryptoKey,
    ctBytes
  );

  // 3) Decode back to a UTF-8 string
  return new TextDecoder().decode(plainBuffer);
}

// ── Example Usage ──
(async () => {
  // 1) First derive an AES key via PBKDF2 (reuse from section 2)
  const password = "Pa$$w0rd";
  const saltB64  = randomSaltBase64();
  const key      = await deriveAesKeyFromPassword(password, saltB64);

  // 2) Encrypt a message
  const message = "Cardano loves AES-GCM!";
  const { iv, ciphertext } = await aesGcmEncrypt(message, key);
  console.log("IV (Base64):", iv);
  console.log("Encrypted (Base64):", ciphertext);

  // 3) Later… decrypt it
  const recovered = await aesGcmDecrypt(iv, ciphertext, key);
  console.log("Decrypted plaintext:", recovered);
  // => "Cardano loves AES-GCM!"
})();

4. ed25519 (Edwards-curve Digital Signature Algorithm)

What it is

ed25519 is a modern, high-performance elliptic-curve digital signature scheme (Curve25519). It lets you:

  • Generate a private key (secret) + public key (32 bytes).
  • Sign messages with the private key.
  • Verify signatures with the public key.

Why it’s useful

  • Cardano uses ed25519 for transaction signing and wallet authentication.
  • It’s fast, compact, and secure against known attacks.

How to use it (via TweetNaCl.js)

We’ll load TweetNaCl from a CDN. It’s a small library that implements ed25519 in pure JS.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>ed25519 Demo (TweetNaCl)</title>
  <script src="https://unpkg.com/tweetnacl@1.0.3/nacl.min.js"></script>
  <script src="https://unpkg.com/tweetnacl-util@0.15.1/nacl-util.min.js"></script>
</head>
<body>
  <h2>ed25519 Sign/Verify Demo</h2>
  <button id="genKey">Generate Keypair</button><br /><br />

  <div id="kpairs" style="white-space: pre; font-family: monospace;"></div>
  <hr />

  <textarea id="msg" rows="3" cols="50">Hello, Cardano!</textarea><br />
  <button id="signBtn">Sign Message</button><br /><br />

  <div id="signature" style="white-space: pre; font-family: monospace;"></div>
  <hr />

  <button id="verifyBtn">Verify Signature</button>
  <div id="verifyResult"></div>

  <script>
    let keyPair = null; // will hold { publicKey, secretKey } (Uint8Array)
    let sigBase64 = "";

    document.getElementById("genKey").onclick = () => {
      // 1) Generate a new ed25519 keypair
      keyPair = nacl.sign.keyPair();
      const pubB64 = nacl.util.encodeBase64(keyPair.publicKey);
      const secB64 = nacl.util.encodeBase64(keyPair.secretKey);

      document.getElementById("kpairs").textContent =
        `Public Key  (Base64): ${pubB64}\n` +
        `Secret Key  (Base64): ${secB64}\n\n` +
        "(Keep the secret key safe! This example shows it in plain text.)";
    };

    document.getElementById("signBtn").onclick = () => {
      if (!keyPair) {
        alert("Generate a keypair first.");
        return;
      }
      const message = document.getElementById("msg").value;
      // 2) Convert message → Uint8Array
      const msgBytes = nacl.util.decodeUTF8(message);
      // 3) Sign with secretKey
      const signature = nacl.sign.detached(msgBytes, keyPair.secretKey);
      sigBase64 = nacl.util.encodeBase64(signature);
      document.getElementById("signature").textContent =
        `Signature (Base64): ${sigBase64}`;
    };

    document.getElementById("verifyBtn").onclick = () => {
      if (!keyPair) {
        alert("Generate keypair & sign first.");
        return;
      }
      const message = document.getElementById("msg").value;
      const msgBytes = nacl.util.decodeUTF8(message);
      // 4) Convert Base64 sig → Uint8Array
      const sigBytes = nacl.util.decodeBase64(sigBase64);
      // 5) Verify: returns true/false
      const isValid = nacl.sign.detached.verify(
        msgBytes,
        sigBytes,
        keyPair.publicKey
      );
      document.getElementById("verifyResult").textContent =
        isValid ? "✅ Signature is valid!" : "❌ Signature is invalid.";
    };
  </script>
</body>
</html>

5. BLAKE2b (Blake2-b Hash)

What it is

BLAKE2b is a fast, secure cryptographic hash function, optimized for 64-bit platforms. It produces variable-length outputs, commonly 256 bits (32 bytes) or 512 bits (64 bytes). Cardano uses BLAKE2b to hash transaction bodies, scripts, and address payloads (e.g., payment/stake key hashes).

Why it’s useful

  • Cardano addresses are built by hashing a public key with BLAKE2b-224 (224 bits) or BLAKE2b-256 (256 bits), then encoding via Bech32.
  • Metadata in Cardano transactions is hashed with BLAKE2b when computing the transaction ID.
  • BLAKE2b is faster and lighter than SHA-2 on modern CPUs.

How to use it (via @noble/hashes)

The Web Crypto API currently does not support BLAKE2b natively. We'll use a small JS library.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>BLAKE2b Demo (@noble/hashes)</title>
  <script type="module">
    import { blake2b } from "https://cdn.jsdelivr.net/npm/@noble/hashes@1.1.0/blake2b.js";
    import { hex } from "https://cdn.jsdelivr.net/npm/@noble/hashes@1.1.0/utils.js";

    async function demoBlake2b() {
      const message = "Cardano Loves Blake2b!";
      // Convert string to Uint8Array
      const msgBytes = new TextEncoder().encode(message);

      // Compute a 256-bit (32-byte) Blake2b hash
      const hashBytes = blake2b(msgBytes, { dkLen: 32 });
      const hashHex = hex(hashBytes);
      console.log(`BLAKE2b-256("${message}") = ${hashHex}`);

      // If you need a 224-bit digest (28 bytes):
      const hash224 = blake2b(msgBytes, { dkLen: 28 });
      console.log(`BLAKE2b-224("${message}") = ${hex(hash224)}`);
    }

    demoBlake2b();
  </script>
</head>
<body>
  <h2>BLAKE2b Demo</h2>
  <p>Open the console to see the output.</p>
</body>
</html>

Key points:

  • blake2b(msgBytes, { dkLen: 32 }) returns a 32-byte digest (BLAKE2b-256).

  • Cardano uses BLAKE2b-224 (28 bytes) for payment and stake key hashes in addresses. For example:

    const keyHash = blake2b(pubKeyBytes, { dkLen: 28 });  
    // then Bech32-encode that keyHash as part of the address.

6. Bech32 (Human-Readable Address Encoding)

What it is

Bech32 is a checksummed Base32 encoding, designed to produce human-readable strings that are easy to type and spot transcription errors. Cardano uses Bech32 (with “addr” or “stake” prefixes) to represent on-chain addresses.

A Bech32 string looks like:

addr1q9l3pl... (mainnet payment address)  
addr_test1qpz... (testnet payment address)  
stake1u8k4...  (stake address)  

Why it’s useful

  • Error detection: Built-in checksum catches most typos.
  • Segregation: Prefix (“addr”, “addr_test”, “stake”) tells you if it’s a payment address or stake address, and which network.
  • Readability: All lowercase, no confusing characters (e.g., no “1” vs. “l”).

How to encode/decode Bech32 (via bech32 library)

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Bech32 Demo (bitcoinjs/bech32)</title>
  <script type="module">
    import { bech32 } from "https://cdn.jsdelivr.net/npm/bech32@1.1.4/index.min.js";

    // Convert a Payment Key Hash (Uint8Array) to a Bech32 address (simple example)
    function encodeBech32(prefix, dataBytes) {
      // 1) Convert dataBytes (8-bit) → 5-bit words via bech32.toWords
      const words = bech32.toWords(dataBytes);
      // 2) Encode with prefix + words
      return bech32.encode(prefix, words);
    }

    // Decode a Bech32 string → { prefix, dataBytes }
    function decodeBech32(addr) {
      const { prefix, words } = bech32.decode(addr);
      const dataBytes = bech32.fromWords(words); // back to Uint8Array
      return { prefix, dataBytes };
    }

    // ── Example Usage ──
    const sampleBytes = Uint8Array.from([
      0x00, 0x14, 0x75, 0xa3, 0x62, 0x3b, 0x1d, 0x4a,
      0x2f, 0x99, 0x54, 0x06, 0x7b, 0x9f, 0x46, 0xde,
      0x5f, 0x6a, 0x20, 0x9f, 0x7a
    ]); // e.g. script or key hash + address header  
    const bech = encodeBech32("addr_test", sampleBytes);
    console.log("Bech32 Encoded:", bech);

    const { prefix, dataBytes } = decodeBech32(bech);
    console.log("Decoded prefix:", prefix);
    console.log("Decoded bytes:", dataBytes);
  </script>
</head>
<body>
  <h2>Bech32 Demo</h2>
  <p>Open the console to see the output.</p>
</body>
</html>

How Cardano uses it

  1. Compute key hash: BLAKE2b-224 of a public key ⇒ 28 bytes.

  2. Build address payload: A version byte (e.g., 0x01 for base payment) + payment key hash + stake key hash (for a base address).

  3. Encode with Bech32:

    const payload = new Uint8Array([0x01, ...paymentKeyHash, ...stakeKeyHash]);
    const address = encodeBech32("addr_test", payload);
    console.log(address);
    // e.g. "addr_test1qpzmd..." 

7. CIP-1852 HD Key Derivation (Extended ed25519 Keys)

What it is

Cardano Improvement Proposal 1852 defines a Hierarchical Deterministic (HD) wallet scheme using ed25519 extended keys (ed25519-BIP32 style). From a single 12- or 24-word BIP39 mnemonic, you can deterministically derive multiple account, payment, and stake key pairs.

Why it’s useful

  • Single mnemonic → many keys: All your addresses come from a single seed phrase.
  • Standardized: Wallets like Daedalus, Yoroi, Nami, etc. follow CIP-1852.
  • Security: You only need to back up your mnemonic.

How it works (overview)

  1. BIP39 mnemonic (12 or 24 words) → 512-bit seed.

  2. CIP-1852 path: m / 1852' / 1815' / account' / role / index

    • 1852' = CIP-1852 purpose
    • 1815' = Cardano coin type (registered with SLIP-44)
    • account' = account index (hardened)
    • role = 0 for external (payment), 1 for internal (change), 2 for stake
    • index = address index (non-hardened)
  3. ed25519 extended derivation: Use HMAC-SHA512 (per Ed25519-BIP32) to derive child keys.

A full HD derivation implementation is quite involved. Below is a simplified illustration for generating a root key from a mnemonic, then deriving one child key. In real wallets, you’d use a dedicated library (e.g., cardano-crypto.js, cardano-crypto.js Noble edition or @emurgo/cardano-serialization-lib).

import { mnemonicToSeedSync } from "https://cdn.jsdelivr.net/npm/bip39@3.1.0/src/index.min.js";
import { hmac } from "https://cdn.jsdelivr.net/npm/@noble/hashes@1.1.0/hmac.js";
import { sha512 } from "https://cdn.jsdelivr.net/npm/@noble/hashes@1.1.0/sha512.js";
import * as ed from "https://cdn.jsdelivr.net/npm/@noble/ed25519@1.7.1/lib/esm/index.js";

// 1) Convert mnemonic → 64-byte seed (BIP39 uses PBKDF2 internally)
const mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
const seed = mnemonicToSeedSync(mnemonic); // Buffer(64)

// 2) Compute root key (ed25519 extended):
function getRootMasterKey(seed64) {
  // Per CIP-1852 (similar to Ed25519-BIP32):
  // key = HMAC-SHA512("ed25519 seed", seed)
  const I = hmac(sha512, Buffer.from("ed25519 seed", "utf8"))(seed64);
  const il = I.slice(0, 32);   // master secret key
  const ir = I.slice(32, 64);  // master chain code
  return { key: il, chainCode: ir };
}

const root = getRootMasterKey(seed);
console.log("Root secret key (hex):", Buffer.from(root.key).toString("hex"));
console.log("Root chain code  (hex):", Buffer.from(root.chainCode).toString("hex"));

// 3) Derive a child at path m/1852'/1815'/0'/0/0 
//    (account 0, external (0), address index 0)
async function deriveChild(parentKey, parentChain, index, isHardened) {
  // Cardano uses hardened derivation up to "account'". After that, external/internal indexes are non-hardened.
  // For hardened: index >= 0x80000000
  const data = isHardened
    ? Buffer.concat([Buffer.alloc(1, 0), parentKey, toBigEndian(index | 0x80000000)])
    : Buffer.concat([await ed.getPublicKey(parentKey), toBigEndian(index)]);
  // HMAC-SHA512 with parentChain as key
  const I = hmac(sha512, parentChain)(data);
  const il = I.slice(0, 32);   // child secret key fragment
  const ir = I.slice(32, 64);  // child chain code
  // In full BIP32-Ed25519, you also add parent public key to il for non-hardened,
  // but Cardano uses only hardened for account, then simple ed25519 for external/internal
  return { key: il, chainCode: ir };
}

// Helper: convert 32-bit integer to 4-byte Buffer (big-endian)
function toBigEndian(num) {
  const buf = Buffer.alloc(4);
  buf.writeUInt32BE(num, 0);
  return buf;
}

(async () => {
  // Derive m/1852'
  const level1 = await deriveChild(root.key, root.chainCode, 1852, true);
  // Derive m/1852'/1815'
  const level2 = await deriveChild(level1.key, level1.chainCode, 1815, true);
  // Derive m/1852'/1815'/0' (account 0)
  const level3 = await deriveChild(level2.key, level2.chainCode, 0, true);
  // Derive m/1852'/1815'/0'/0 (external chain)
  const level4 = await deriveChild(level3.key, level3.chainCode, 0, false);
  // Derive m/1852'/1815'/0'/0/0 (address index 0)
  const level5 = await deriveChild(level4.key, level4.chainCode, 0, false);

  console.log("Payment secret key (hex):", Buffer.from(level5.key).toString("hex"));
  // Public key
  const paymentPub = await ed.getPublicKey(level5.key);
  console.log("Payment public key (hex):", Buffer.from(paymentPub).toString("hex"));
})();

Note: In production, you’ll typically use a well-tested library instead of writing all the derivation logic by hand. This snippet illustrates the rough flow:

  1. BIP39 mnemonic → seed (64 bytes) via PBKDF2.
  2. Root master key = HMAC-SHA512("ed25519 seed", seed).
  3. Derive hardened children (index | 0x80000000) up to the account level.
  4. Then derive non-hardened “internal/external” and “address index” keys.
  5. Use noble-ed25519 to compute the public key from each secret key fragment.

8. Putting It All Together: Cardano-Flavored Secure Notes

In this final section, we combine everything above to build a “Secure Notes” demo that:

  1. Derives an AES-GCM key from a user’s password (via PBKDF2).
  2. Encrypts note content with AES-GCM for confidentiality.
  3. Hashes note titles with SHA-256.
  4. Signs the ciphertext with an ed25519 key for integrity.
  5. Uses BLAKE2b to fingerprint some payload (e.g., an address or metadata).
  6. Encodes an example Cardano address via Bech32.
  7. (Optionally) Derives an ed25519 key from a BIP39 mnemonic using CIP-1852.
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Cardano-Flavored Secure Notes</title>
  <script src="https://unpkg.com/tweetnacl@1.0.3/nacl.min.js"></script>
  <script src="https://unpkg.com/tweetnacl-util@0.15.1/nacl-util.min.js"></script>
  <script type="module">
    import { blake2b } from "https://cdn.jsdelivr.net/npm/@noble/hashes@1.1.0/blake2b.js";
    import { hex } from "https://cdn.jsdelivr.net/npm/@noble/hashes@1.1.0/utils.js";
    import { bech32 } from "https://cdn.jsdelivr.net/npm/bech32@1.1.4/index.min.js";

    // ─── Utility Functions ──────────────────────────────────────────────────

    // 1) SHA-256 (same as Section 1)
    async function sha256Hex(str) {
      const buf = new TextEncoder().encode(str);
      const hash = await crypto.subtle.digest("SHA-256", buf);
      return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, "0")).join("");
    }

    // 2) PBKDF2 → AES-GCM key (same as Section 2)
    async function deriveKey(password, saltB64) {
      const pwBytes = new TextEncoder().encode(password);
      const baseKey = await crypto.subtle.importKey(
        "raw", pwBytes, { name: "PBKDF2" }, false, ["deriveKey"]
      );
      const saltBytes = Uint8Array.from(atob(saltB64), c => c.charCodeAt(0));
      return crypto.subtle.deriveKey(
        { name: "PBKDF2", salt: saltBytes, iterations: 100_000, hash: "SHA-256" },
        baseKey,
        { name: "AES-GCM", length: 256 },
        true,
        ["encrypt", "decrypt"]
      );
    }
    function randomSaltB64() {
      const s = crypto.getRandomValues(new Uint8Array(16));
      return btoa(String.fromCharCode(...s));
    }

    // 3) AES-GCM encrypt/decrypt (same as Section 3)
    async function aesGcmEncryptUTF8(plaintext, key) {
      const pt = new TextEncoder().encode(plaintext);
      const iv = crypto.getRandomValues(new Uint8Array(12));
      const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, pt);
      return {
        iv: btoa(String.fromCharCode(...iv)),
        ciphertext: btoa(String.fromCharCode(...new Uint8Array(ct)))
      };
    }
    async function aesGcmDecryptUTF8(ivB64, ctB64, key) {
      const ivBytes = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0));
      const ctBytes = Uint8Array.from(atob(ctB64), c => c.charCodeAt(0));
      const ptBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv: ivBytes }, key, ctBytes);
      return new TextDecoder().decode(ptBuf);
    }

    // 4) ed25519 sign/verify (same as Section 4)
    function genEd25519KeyPair() {
      return nacl.sign.keyPair();
    }
    function signEd25519(messageStr, secretKey) {
      const mb = nacl.util.decodeUTF8(messageStr);
      const sig = nacl.sign.detached(mb, secretKey);
      return nacl.util.encodeBase64(sig);
    }
    function verifyEd25519(messageStr, sigB64, publicKey) {
      const mb = nacl.util.decodeUTF8(messageStr);
      const sb = nacl.util.decodeBase64(sigB64);
      return nacl.sign.detached.verify(mb, sb, publicKey);
    }

    // 5) BLAKE2b (same as Section 5)
    function blake2bHex(message, outLen = 32) {
      const msgBytes = new TextEncoder().encode(message);
      const digest = blake2b(msgBytes, { dkLen: outLen });
      return hex(digest);
    }

    // 6) Bech32 encode/decode (same as Section 6)
    function encodeBech32(prefix, dataBytes) {
      const words = bech32.toWords(dataBytes);
      return bech32.encode(prefix, words);
    }
    function decodeBech32(addr) {
      const { prefix, words } = bech32.decode(addr);
      return { prefix, dataBytes: bech32.fromWords(words) };
    }

    // 7) (CIP-1852 HD derivation is complex—omitted for brevity—but you could call into a library here.)

    // ──────────────────────────────────────────────────────────────────────────

    // We'll store a single note in memory for this demo:
    let savedNote = null;

    document.addEventListener("DOMContentLoaded", () => {
      const saveBtn       = document.getElementById("saveBtn");
      const loadBtn       = document.getElementById("loadBtn");
      const out           = document.getElementById("output");
      const loadedOut     = document.getElementById("loadedOutput");

      saveBtn.onclick = async () => {
        out.textContent = "";

        // 1) Gather user input
        const title   = document.getElementById("noteTitle").value.trim();
        const content = document.getElementById("noteContent").value;
        const pwd     = document.getElementById("pwd1").value;
        if (!title || !content || !pwd) {
          alert("Please fill in title, content, and password.");
          return;
        }

        // 2) Hash the title with SHA-256
        const titleHash = await sha256Hex(title);
        out.textContent += `Title SHA-256: ${titleHash}\n`;

        // 3) Derive AES-GCM key from password + new salt
        const saltB64 = randomSaltB64();
        const aesKey  = await deriveKey(pwd, saltB64);
        out.textContent += `Generated salt (Base64): ${saltB64}\n`;

        // 4) Encrypt the note’s content under AES-GCM
        const { iv, ciphertext } = await aesGcmEncryptUTF8(content, aesKey);
        out.textContent += `Encrypted content (Base64): ${ciphertext}\n`;
        out.textContent += `IV (Base64): ${iv}\n`;

        // 5) Sign the ciphertext with ed25519 for integrity
        const kp        = genEd25519KeyPair();
        const sigB64    = signEd25519(ciphertext, kp.secretKey);
        const pubEdB64  = nacl.util.encodeBase64(kp.publicKey);
        out.textContent += `ed25519 Public Key (Base64): ${pubEdB64}\n`;
        out.textContent += `Signature (Base64): ${sigB64}\n`;

        // 6) Compute a Blake2b-224 fingerprint of the ciphertext (optional)
        const fptBlake2b224 = blake2bHex(ciphertext, 28);
        out.textContent += `BLAKE2b-224(ciphertext) = ${fptBlake2b224}\n`;

        // 7) Example: Bech32-encode a dummy 28-byte key hash
        const dummyKeyHash = Uint8Array.from(
          blake2bHex("example", 28).match(/.{2}/g).map(h => parseInt(h, 16))
        );
        const exampleAddr = encodeBech32("addr_test", Uint8Array.from([0x01, ...dummyKeyHash, ...dummyKeyHash]));
        out.textContent += `Example Cardano (Testnet) Address: ${exampleAddr}\n`;

        // 8) Save everything in memory
        savedNote = {
          titleHash,
          saltB64,
          iv,
          ciphertext,
          edPubB64: pubEdB64,
          signatureB64: sigB64,
          blake2b224: fptBlake2b224,
          exampleAddr
        };

        out.textContent += "\n▶ Note saved (encrypted, signed, hashed)!\n";
      };

      loadBtn.onclick = async () => {
        loadedOut.textContent = "";
        if (!savedNote) {
          alert("No note saved yet!");
          return;
        }
        const pwd = document.getElementById("pwd2").value;
        if (!pwd) {
          alert("Enter the password to decrypt.");
          return;
        }

        // 1) Verify the signature before decrypting
        const isValid = verifyEd25519(
          savedNote.ciphertext,
          savedNote.signatureB64,
          nacl.util.decodeBase64(savedNote.edPubB64)
        );
        loadedOut.textContent += isValid
          ? "✅ Signature is valid (ciphertext intact).\n"
          : "❌ Signature invalid! Aborting.\n";
        if (!isValid) return;

        // 2) Re-derive AES key from password + stored salt
        let aesKey;
        try {
          aesKey = await deriveKey(pwd, savedNote.saltB64);
        } catch (err) {
          loadedOut.textContent += "❌ Key derivation failed (wrong password).\n";
          return;
        }

        // 3) Decrypt
        try {
          const plaintext = await aesGcmDecryptUTF8(
            savedNote.iv,
            savedNote.ciphertext,
            aesKey
          );
          loadedOut.textContent += `Decrypted content: ${plaintext}\n`;
        } catch (err) {
          loadedOut.textContent += "❌ Decryption failed (wrong password or tampered data).\n";
        }

        // 4) Show Blake2b-224 fingerprint and example address
        loadedOut.textContent += `Stored BLAKE2b-224 of ciphertext: ${savedNote.blake2b224}\n`;
        loadedOut.textContent += `Stored example address: ${savedNote.exampleAddr}\n`;
      };
    });
  </script>
</head>
<body>
  <h2>Cardano-Flavored Secure Notes Demo</h2>
  <label>Note Title: <input type="text" id="noteTitle" value="My Secret Note"></label><br />
  <label>Note Content:<br />
    <textarea id="noteContent" rows="4" cols="60">This note is stored encrypted on the client.</textarea>
  </label><br />
  <label>Password: <input type="password" id="pwd1" /></label><br /><br />

  <button id="saveBtn">Save & Encrypt</button>
  <pre id="output"></pre>
  <hr />

  <label>Enter password to decrypt: <input type="password" id="pwd2" /></label><br />
  <button id="loadBtn">Load & Decrypt</button>
  <pre id="loadedOutput"></pre>
</body>
</html>

How it all fits (Cardano edition):

  1. SHA-256 hashes the note title so you can verify “title integrity” without revealing the full title.
  2. PBKDF2 → AES-GCM encrypts your note content with a password (you see the Base64 IV + ciphertext).
  3. ed25519 signs the ciphertext so you know if someone alters it.
  4. BLAKE2b computes a 224-bit fingerprint of the ciphertext (similar to how Cardano computes key hashes for addresses).
  5. Bech32 encodes a dummy Cardano address (for demonstration). In real wallets, you’d compute a real key hash, then Bech32-encode it as shown.
  6. Optionally, you could derive the ed25519 keypair from a BIP39 mnemonic (CIP-1852), but in this demo we generate a fresh random key.

Glossary of Terms

  • AES-GCM: A symmetric encryption mode (AES in Galois/Counter Mode) providing both confidentiality and integrity. Uses a random 12-byte IV per encryption.
  • BLAKE2b: A fast, secure cryptographic hash function. Cardano uses BLAKE2b-224 to hash public keys when building addresses.
  • Bech32: A checksummed Base32 encoding for addresses, producing human-readable strings with built-in error detection (e.g., addr1q...).
  • CBOR: Concise Binary Object Representation, a compact binary data format. Cardano serializes transactions and scripts in CBOR before hashing.
  • CIP-1852: Cardano Improvement Proposal 1852, defining an HD (hierarchical deterministic) wallet scheme using ed25519 extended keys. From a single 12/24-word BIP39 mnemonic, you derive multiple payment/stake keypairs.
  • Ed25519: An elliptic-curve digital signature scheme (Curve25519) used by Cardano for signing transactions and wallet authentication.
  • HMAC-SHA512: Hash-based Message Authentication Code using SHA-512. In ed25519-BIP32 (CIP-1852), it’s used to derive child key material from a parent chain code.
  • HD Wallet: Hierarchical deterministic wallet; a system that deterministically derives many keypairs from a single seed (e.g., a BIP39 mnemonic).
  • PBKDF2: Password-Based Key Derivation Function 2. Takes a password + salt + iteration count + hash function (e.g., SHA-256) and produces a cryptographic key (e.g., for AES).
  • Salt: Random data mixed into a password before hashing to ensure that identical passwords produce different hashes. Typically 16 bytes for PBKDF2.
  • SHA-256: Secure Hash Algorithm producing a 256-bit digest. Common general-purpose hash. Cardano mostly uses BLAKE2b, but SHA-256 appears in some metadata hashing or auxiliary contexts.
  • Signature (ed25519): A 64-byte (Base64-encoded 88-character) string produced by signing a message with an ed25519 private key. Verifiable with the corresponding public key.
  • Transaction ID: In Cardano, the transaction body is CBOR-serialized, then hashed with BLAKE2b-256 to produce the TX ID (TxHash).
  • Uint8Array: JavaScript typed array representing an array of 8-bit unsigned integers (bytes). Used pervasively when converting between strings, binary data, and cryptographic functions.

Clone this wiki locally