-
Notifications
You must be signed in to change notification settings - Fork 4
2. Bonus Tutorials : Introduction to Cryptography
- SHA-256 (Secure Hash Algorithm 256-bit)
- PBKDF2 (Password-Based Key Derivation Function 2)
- AES-GCM (Advanced Encryption Standard in Galois/Counter Mode)
- ed25519 (Edwards-curve Digital Signature Algorithm)
- BLAKE2b (Blake2-b Hash)
- Bech32 (Human-Readable Address Encoding)
- CIP-1852 HD Key Derivation (Extended ed25519 Keys)
- Putting It All Together: Cardano-Flavored Secure Notes
- Glossary of Terms
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).
- 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).
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)
})();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.
- 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).
// 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);
})();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.
- 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.
// 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!"
})();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.
- 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>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).
- 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.
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)
- 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
-
Compute key hash: BLAKE2b-224 of a public key ⇒ 28 bytes.
-
Build address payload: A version byte (e.g., 0x01 for base payment) + payment key hash + stake key hash (for a base address).
-
Encode with Bech32:
const payload = new Uint8Array([0x01, ...paymentKeyHash, ...stakeKeyHash]); const address = encodeBech32("addr_test", payload); console.log(address); // e.g. "addr_test1qpzmd..."
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.
- 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.
-
BIP39 mnemonic (12 or 24 words) → 512-bit seed.
-
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)
-
-
ed25519 extended derivation: Use HMAC-SHA512 (per Ed25519-BIP32) to derive child keys.
Example with @noble/hashes + @noble/ed25519
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:
- BIP39 mnemonic → seed (64 bytes) via PBKDF2.
- Root master key = HMAC-SHA512("ed25519 seed", seed).
- Derive hardened children (
index | 0x80000000) up to the account level.- Then derive non-hardened “internal/external” and “address index” keys.
- Use
noble-ed25519to compute the public key from each secret key fragment.
In this final section, we combine everything above to build a “Secure Notes” demo that:
- Derives an AES-GCM key from a user’s password (via PBKDF2).
- Encrypts note content with AES-GCM for confidentiality.
- Hashes note titles with SHA-256.
- Signs the ciphertext with an ed25519 key for integrity.
- Uses BLAKE2b to fingerprint some payload (e.g., an address or metadata).
- Encodes an example Cardano address via Bech32.
- (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):
- SHA-256 hashes the note title so you can verify “title integrity” without revealing the full title.
- PBKDF2 → AES-GCM encrypts your note content with a password (you see the Base64 IV + ciphertext).
- ed25519 signs the ciphertext so you know if someone alters it.
- BLAKE2b computes a 224-bit fingerprint of the ciphertext (similar to how Cardano computes key hashes for addresses).
- Bech32 encodes a dummy Cardano address (for demonstration). In real wallets, you’d compute a real key hash, then Bech32-encode it as shown.
- Optionally, you could derive the ed25519 keypair from a BIP39 mnemonic (CIP-1852), but in this demo we generate a fresh random key.
- 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.
Bernard Sibanda is a global Technology Entrepreneur, Web3 and Software Consultant with a deep focus on Cardano Blockchain, Midnight and Community building.
Key Positions:
- Founder, CTO, Developer Advocate cohort #1, Fullstake Developer, Cardano Ambassador, Catalyst Project Manager, DREP-WIMS:
- Co-founder of ABL Tech and Cardano Africa Live
- EBU-certified Plutus Pioneer (Plutus/Haskell)
- Cohort #1 Plutus Pioneer Developer
- Catalyst Community Reviewer & Funded Projects Manager
-
DRep for WIMS-Cardano (ID:
drep1yguj8zu48n99pv70yl6ckzt9hdgjy8yjnlqs2uyzcpafnjgu4vkul) - Intersect Developer Advocate
- Intersect Committe Member 2025-2026
- Cardano Marketer,Promoter and blogger
- Cardano Open Source Contributor
- Cardano communities and events organizer and builder
- Cardano Ambassador for South Africa
Official links:
- Stablecoins Dex
- Coxygen Global Universities
- WIMS Cardano Global
- Cardano Africa Live
- WIMS Cardano Videos
- Cardano Smart Contract Videos
- Fullstack IT Consulting
Social links: