Unlocking Secure Communication and Mastering Encryption with Kaspa WASM SDK
A hands‑on guide to symmetric encryption, Diffie–Hellman key exchange, and digital signatures in the browser
Full demo with all the files can be downloaded from the minKasWasm repo on github. If you missed it, here’s A Beginner’s Guide to Mastering Kaspa WASM SDK in the Browser.
Brief Introduction to Encryption
Encryption is the process of transforming readable data (plaintext) into an unreadable format (ciphertext) so that only authorized parties can access it. It’s a cornerstone of secure communication.
There are two broad categories:
Symmetric encryption
Uses the same key for both encryption and decryption.
Fast and efficient, but requires secure ke
y sharing.
Example: AES, XChaCha20-Poly1305.
Asymmetric encryption / Diffie–Hellman key exchange
Uses two keys: a private key and a public key.
Diffie–Hellman allows two parties to agree on a shared secret over an insecure channel.
Once the shared secret is established, it can be used for symmetric encryption.
Often paired with digital signatures to prove authenticity.
Symmetric Encryption Demo (encryption.html)
This demo shows how to encrypt and decrypt text with a password using the XChaCha20Poly1305 algorithm provided by the Kaspa WASM SDK.
What is XChaCha20Poly1305?
ChaCha20 → a stream cipher designed by Daniel J. Bernstein, optimized for speed and security even on devices without AES hardware acceleration.
Poly1305 → a message authentication code (MAC) algorithm, used to verify integrity and authenticity of the ciphertext.
XChaCha20 → an extended version of ChaCha20 that uses a 192‑bit nonce (instead of 96‑bit), making it safer for large‑scale systems where random nonces might repeat.
Together, XChaCha20-Poly1305 is an AEAD (Authenticated Encryption with Associated Data) construction:
It encrypts data (confidentiality).
It authenticates data (integrity + authenticity).
Steps:
Open
encryption.htmlin your browser.Enter a password in the password field.
This acts as the symmetric key.
Enter some text in the textarea.
Click Encrypt.
The text is encrypted using your password.
The result is shown in JSON format (structured payload).
Copy the encrypted output into the textarea. Example:
Result:
{
"version": 1,
"cipherText": "C8nZgVk3PtYP6URssHgIZ9a5Sz2h7h9000Le2ShlIf2q+Wx0Td41w4bDAVrAuG1nyDCCgAlr0Kigp/yG1sAwTdCFVnEX9TkF4fGNxYQIfzeCcH7mwWY4NRFdIkfHUpSd8VzyA7zVbkXwc2RamAjNV2r5b61yAqgPnJb2A47FK7uTXHNkDIAQKStjPEpHSpkeyyccNfO5n4Ow"
}
You want to copy:
"C8nZgVk3PtYP6URssHgIZ9a5Sz2h7h9000Le2ShlIf2q+Wx0Td41w4bDAVrAuG1nyDCCgAlr0Kigp/yG1sAwTdCFVnEX9TkF4fGNxYQIfzeCcH7mwWY4NRFdIkfHUpSd8VzyA7zVbkXwc2RamAjNV2r5b61yAqgPnJb2A47FK7uTXHNkDIAQKStjPEpHSpkeyyccNfO5n4Ow+adHiYI="
Password = 1234 to see my corny inspirational messageClick Decrypt.
Enter the same password.
The original plaintext is recovered.
What you’ve learned:
How symmetric encryption requires the same password for both operations.
If the wrong password is used, decryption fails.
encryption.js
// encryption.js
import {
encryptXChaCha20Poly1305,
decryptXChaCha20Poly1305
} from "./kas-wasm/kaspa.js";
/**
* Enterprise-grade encryption wrapper using Kaspa WASM XChaCha20-Poly1305
* @param {string} plaintext - The message to encrypt
* @param {string} password - The password/key material
* @returns {object} { version, cipherText }
*/
export function encryptMessage(plaintext, password) {
if (typeof plaintext !== "string" || typeof password !== "string") {
throw new TypeError("encryptMessage requires string inputs");
}
try {
const cipherText = encryptXChaCha20Poly1305(plaintext, password);
return {
version: 1, // bump if you change format later
cipherText
};
} catch (err) {
throw new Error(`Encryption failed: ${err.message}`);
}
}
/**
* Enterprise-grade decryption wrapper using Kaspa WASM XChaCha20-Poly1305
* @param {object|string} payload - Either raw cipherText string or {version, cipherText}
* @param {string} password - The password/key material
* @returns {string} plaintext
*/
export function decryptMessage(payload, password) {
if (!password || typeof password !== "string") {
throw new TypeError("decryptMessage requires a string password");
}
let cipherText;
if (typeof payload === "string") {
cipherText = payload;
} else if (payload && typeof payload === "object" && payload.cipherText) {
if (payload.version !== 1) {
throw new Error(`Unsupported payload version: ${payload.version}`);
}
cipherText = payload.cipherText;
} else {
throw new TypeError("decryptMessage requires a cipherText string or payload object");
}
try {
return decryptXChaCha20Poly1305(cipherText, password);
} catch (err) {
throw new Error(`Decryption failed: ${err.message}`);
}
}GUI portion:
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="styles.css">
<meta charset="UTF-8">
<title>Symmetric Encryption Demo</title>
</head>
<body>
<h1>Symmetric Encryption Example</h1>
<label>Password:</label>
<input type="text" id="password" placeholder="Enter password">
<label>Text:</label>
<textarea id="plaintext" placeholder="Enter text to encrypt/decrypt"></textarea>
<button id="encryptBtn">Encrypt</button>
<button id="decryptBtn">Decrypt</button>
<div class="output">
<strong>Result:</strong>
<pre id="result"></pre>
</div>
<script type="module">
import initKaspa from "./kas-wasm/kaspa.js";
import { encryptMessage, decryptMessage } from "./encryption.js";
await initKaspa();
document.getElementById("encryptBtn").addEventListener("click", () => {
const password = document.getElementById("password").value;
const text = document.getElementById("plaintext").value;
try {
const encrypted = encryptMessage(text, password);
document.getElementById("result").textContent = JSON.stringify(encrypted, null, 2);
} catch (err) {
document.getElementById("result").textContent = "Encryption error: " + err.message;
}
});
document.getElementById("decryptBtn").addEventListener("click", () => {
const password = document.getElementById("password").value;
const text = document.getElementById("plaintext").value;
try {
let payload;
try {
payload = JSON.parse(text);
} catch {
payload = text;
}
const decrypted = decryptMessage(payload, password);
document.getElementById("result").textContent = decrypted;
} catch (err) {
document.getElementById("result").textContent = "Decryption error: " + err.message;
}
});
</script>
</body>
</html>Diffie–Hellman + Kaspa Wallet Demo (dh_encryption.html)
This demo integrates Kaspa wallet keys with Diffie–Hellman to establish a shared secret, encrypt messages, and sign/verify them.
This one gets a lot more complex with private and public keys that can be in the form of bytes or a hex encoded string or a PrivateKey/PublicKey object from the WASM SDK.
Steps:
a. Wallet setup
Enter a wallet password (default
1234).Click Create Wallet.
A mnemonic and first address are generated.
Click Generate New Address.
A new address and keypair are created.
Your public key is displayed.
b. Handshake
Copy your public key and share it with a peer.
Paste the peer’s public key into the Peer Public Key field.
Click Initiate Handshake or Respond to Handshake.
A shared secret is established between you and the peer.
c. Message encryption
Type a message in the Message box.
Click Encrypt.
The message is encrypted using the shared secret.
Copy the ciphertext and paste it back into the box.
Click Decrypt.
The original message is recovered.
d. Message signing
Enter a message in the Sign Message box.
Click Sign.
A digital signature is generated using your private key.
Share the message, signature, and public key.
Paste them into the fields and click Verify.
The system confirms whether the signature is valid.
What you learn:
How Diffie–Hellman establishes a shared secret without directly sharing passwords.
How symmetric encryption can be layered on top of DH.
How digital signatures prove authenticity and integrity of messages.
dh_encryption.js
// dh_encryption.js
import {
encryptXChaCha20Poly1305,
decryptXChaCha20Poly1305
} from "./kas-wasm/kaspa.js";
import * as utilities from "./utilities.js";
import * as secp from "https://esm.sh/@noble/secp256k1";
/**
* Diffie–Hellman Session Manager
* Handles handshake, shared secret derivation, and message encryption/decryption.
*/
export class DHSession {
constructor() {
this.myPrivateKeyHex = null;
this.myPublicKeyHex = null;
this.myPrivateKeyBytes = null;
this.myPublicKeyBytes = null;
this.sharedSecretBytes = null;
this.sessionKey = null;
this.peerPublicKeyHex = null;
this.peerPublicKeyBytes = null;
}
/**
* Initiate handshake: send your public key to peer
*/
initiateHandshake(privateKeyHex, publicKeyHex) {
if (!privateKeyHex || !publicKeyHex) {
throw new Error("init() requires both private and public key bytes");
}
this.myPrivateKeyHex = privateKeyHex;
this.myPublicKeyHex = publicKeyHex;
this.myPrivateKeyBytes = utilities.hexToBytes(privateKeyHex);
this.myPublicKeyBytes = utilities.hexToBytes(publicKeyHex);
return {
type: "DH_INIT",
publicKey: publicKeyHex,
timestamp: Date.now()
};
}
/**
* Respond to handshake: accept peer public key and derive shared secret
*/
async respondToHandshake(peerPublicKeyHex) {
this.peerPublicKeyHex = peerPublicKeyHex;
this.peerPublicKeyBytes = utilities.hexToBytes(peerPublicKeyHex);
// Derive shared secret using noble
this.sharedSecretBytes = secp.getSharedSecret(this.myPrivateKeyBytes, this.peerPublicKeyBytes, true);
// Derive session key (hash the shared secret)
const digest = await window.crypto.subtle.digest("SHA-256", this.sharedSecretBytes);
const sessionKey = new Uint8Array(digest);
this.sessionKey = sessionKey;
return {
type: "DH_ACK",
publicKey: this.myPublicKeyHex,
timestamp: Date.now()
};
}
/**
* Encrypt a message with the session key
*/
encryptMessage(plaintext) {
if (!this.sessionKey) throw new Error("Session not established");
const sessionKeyHex = utilities.bytesToHex(this.sessionKey);
return encryptXChaCha20Poly1305(plaintext, sessionKeyHex);
}
/**
* Decrypt a message with the session key
*/
decryptMessage(cipherText) {
if (!this.sessionKey) throw new Error("Session not established");
const sessionKeyHex = utilities.bytesToHex(this.sessionKey);
return decryptXChaCha20Poly1305(cipherText, sessionKeyHex);
}
}wallet_services.js
In the create function, right before returning, I’ve added:
// Get extended private key for address derivation and DH encryption
const Xprv = await utilities.getXPrv(mnemonicPhrase);
const xPrvString = Xprv.toString();
// Store XPrv and optionally mnemonic securely in IndexedDB
if (storeMnemonic) {
storeWalletData({ filename, mnemonic: mnemonicPhrase, xprv: xPrvString }, password);
} else {
storeWalletData({ filename, xprv: xPrvString }, password);
}I’ve also added a function to generate a new receiving address:
export async function generateNewAddress(change = false) {
const addr = await wallet.accountsCreateNewAddress({
accountId: accountId,
networkId: wallet.networkId,
addressKind: change ? "change" : "receive"
});
return addr.address;
}And another function to generate a new keypair to use for Diffie-Hellman encryption:
export async function generateNewKeypair(index) {
const xprv = await utilities.getXPrvFromStorage(filename, walletSecret);
const xprvHex = xprv.toString();
const derivedKeyPair = await utilities.deriveReceivingChildKeyPair({xprvHex, index});
return {
privateKey: derivedKeyPair.privateKey,
publicKey: derivedKeyPair.publicKey
};
}utilities.js
I’ve added functions for getting the extended private key from the WASM SDK’s Mnemonic class:
export function getXPrv(mnemonicPhrase, passphrase = null) {
const seed = passphrase
? new Mnemonic(mnemonicPhrase).toSeed(passphrase)
: new Mnemonic(mnemonicPhrase).toSeed();
const xPrv = new XPrv(seed);
return xPrv;
}
export async function getXPrvFromStorage(filename, masterPassword) {
const walletData = await loadWalletData(filename, masterPassword);
const xPrv = XPrv.fromXPrv(walletData.xprv);
return xPrv;
}Then I’ve added a function to generate the keypair using the WASM SDK’s PrivateKeyGenerator and PublicKeyGenerator:
// This network parameter can be "mainnet"/"testnet"
// or a NetworkType.MAINNET (1 = mainnet, 2 = testnet)
export async function deriveReceivingChildKeyPair({xprvHex, network = NETWORK, accountIndex = 0n, index = 0}) {
if (typeof index !== "number" || index < 0) {
throw new Error("Index must be a non-negative integer");
}
// Generate private key
const gen = new PrivateKeyGenerator(xprvHex, false, accountIndex);
const privKey = gen.receiveKey(index);
// Generate public key
const pubKey = privKey.toPublicKey();
// Generate address
const pubGen = PublicKeyGenerator.fromMasterXPrv(xprvHex, false, accountIndex);
const addr = pubGen.receiveAddressAsString(network, index);
return { privateKey: privKey.toString(), publicKey: pubKey.toString(), address: addr };
}I’ve created helper functions for signing/verifying messages, but these are just here for convenience/to make it clearer. You could also just import signMessage/verifyMessage directly from the WASM SDK.
export async function signMessageWithPrivateKeyHex(privateKeyHex, message) {
const signature = await signMessage({privateKey: privateKeyHex, message});
return signature;
}
export async function verifyMessageWithPublicKeyHex(publicKeyHex, message, signatureHex) {
const isValid = await verifyMessage({publicKey: publicKeyHex, message, signature: signatureHex});
return isValid;
}
Alternatively, just import them:
import { signMessage, verifyMessage } from './kas-wasm/kaspa.js';GUI portion:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Kaspa DH Encryption Demo</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Kaspa Wallet + Diffie–Hellman Demo</h1>
<label>Password:</label>
<input type="text" id="walletPassword" placeholder="Enter wallet password" value="1234">
<button id="createWalletBtn">Create Wallet</button>
<div class="output">
<strong>Wallet Info:</strong>
<div id="mnemonic"></div>
<button class="copyBtn">Copy</button>
</div>
<div class="output">
<strong>First Address:</strong>
<div id="firstAddress"></div>
<button class="copyBtn">Copy</button>
</div>
<button id="createNewAddressBtn">Generate New Address</button>
<div class="output">
<strong>New Address:</strong>
<div id="newAddressInfo"></div>
<button class="copyBtn">Copy</button>
</div>
<label>Public Key:</label>
<div class="output">
<div id="pubKeyResult"></div>
<button class="copyBtn">Copy</button>
</div>
<label>Peer Public Key:</label>
<input type="text" id="peerPubKey" placeholder="Paste peer public key hex">
1.<button id="initHandshakeBtn">Initiate Handshake</button>
2.<button id="respondHandshakeBtn">Respond to Handshake</button>
<div class="output">
<strong>Handshake Result:</strong>
<div id="handshakeResult"></div>
</div>
<label>Message:</label>
<textarea id="messageBox" placeholder="Enter message"></textarea>
<button id="encryptBtn">Encrypt</button>
<button id="decryptBtn">Decrypt</button>
<div class="output">
<strong>Message Result:</strong>
<div id="messageResult"></div>
<button class="copyBtn">Copy</button>
</div>
<label>Sign/Verify Message:</label>
<textarea id="signMessageBox" placeholder="Enter message to sign">Test message</textarea>
<input type="text" id="publicKeyBox" placeholder="Public key here">
<input type="text" id="signatureBox" placeholder="Signature here">
<button id="signBtn">Sign</button>
<button id="verifyBtn">Verify</button>
<div class="output">
<strong>Verification Result:</strong>
<div id="verifyResult"></div>
</div>
<script type="module">
import { connect } from "./kaspa_client.js";
import { init, createWallet, generateNewAddress, generateNewKeypair } from "./wallet_service.js";
import { DHSession } from "./dh_encryption.js";
import * as utilities from "./utilities.js";
let dhSession = new DHSession();
let myKeys = { privateKey: null, publicKey: null };
const networkId = "testnet-10";
let rpcClient = await connect(null, networkId);
document.getElementById("createWalletBtn").onclick = async () => {
document.getElementById("mnemonic").textContent = "Creating wallet, this can take a bit of time please be patient ...";
const password = document.getElementById("walletPassword").value;
try {
await init(rpcClient, networkId);
const { mnemonic, address } = await createWallet({ password });
document.getElementById("mnemonic").textContent = mnemonic;
document.getElementById("firstAddress").textContent = address;
} catch (err) {
console.error("Wallet creation error:", err);
document.getElementById("mnemonic").textContent = "Error: " + err.message;
}
};
document.getElementById("createNewAddressBtn").onclick = async () => {
let newAddress;
try {
newAddress = await generateNewAddress();
document.getElementById("newAddressInfo").textContent = newAddress;
} catch (err) {
console.error("Generate new address error:", err);
document.getElementById("newAddressInfo").textContent = "Error: " + err.message;
}
const keypair = await generateNewKeypair();
myKeys.privateKey = keypair.privateKey;
myKeys.publicKey = keypair.publicKey;
document.getElementById("pubKeyResult").textContent = myKeys.publicKey;
};
document.getElementById("initHandshakeBtn").onclick = () => {
const msg = dhSession.initiateHandshake(myKeys.privateKey, myKeys.publicKey);
document.getElementById("handshakeResult").textContent = JSON.stringify({
...msg,
publicKey: myKeys.publicKey
}, null, 2);
};
document.getElementById("respondHandshakeBtn").onclick = async () => {
const peerPubKeyHex = document.getElementById("peerPubKey").value.trim();
const msg = await dhSession.respondToHandshake(peerPubKeyHex);
document.getElementById("handshakeResult").textContent = JSON.stringify(msg, null, 2);
};
document.getElementById("encryptBtn").onclick = () => {
const plaintext = document.getElementById("messageBox").value;
try {
const cipher = dhSession.encryptMessage(plaintext);
document.getElementById("messageResult").textContent = cipher;
// Also fill the sign/verify textarea and public key box
document.getElementById("signMessageBox").value = cipher;
document.getElementById("publicKeyBox").value = myKeys.publicKey;
} catch (err) {
document.getElementById("messageResult").textContent = "Error: " + err.message;
}
};
document.getElementById("decryptBtn").onclick = () => {
const cipher = document.getElementById("messageBox").value;
try {
const plain = dhSession.decryptMessage(cipher);
document.getElementById("messageResult").textContent = plain;
// Also fill the sign/verify textarea and public key box
document.getElementById("signMessageBox").value = plain;
document.getElementById("publicKeyBox").value = myKeys.publicKey;
} catch (err) {
document.getElementById("messageResult").textContent = "Error: " + err.message;
}
};
document.getElementById("signBtn").onclick = async () => {
const message = document.getElementById("signMessageBox").value;
try {
const signatureHex = await utilities.signMessageWithPrivateKeyHex(myKeys.privateKey, message);
document.getElementById("signatureBox").value = signatureHex;
document.getElementById("publicKeyBox").value = myKeys.publicKey;
} catch (err) {
document.getElementById("messageResult").textContent = "Error: " + err.message;
}
};
document.getElementById("verifyBtn").onclick = async () => {
const message = document.getElementById("signMessageBox").value;
const pubKeyHex = document.getElementById("publicKeyBox").value.trim();
const signatureHex = document.getElementById("signatureBox").value.trim();
try {
const isValid = await utilities.verifyMessageWithPublicKeyHex(pubKeyHex, message, signatureHex);
document.getElementById("verifyResult").textContent = isValid ? "Signature is valid" : "Signature is INVALID";
} catch (err) {
document.getElementById("verifyResult").textContent = "Error: " + err.message;
}
};
// Add copy-to-clipboard functionality for all .copyBtn buttons
document.querySelectorAll('.output .copyBtn').forEach(btn => {
btn.onclick = function() {
// Find the first div inside the same .output container (skip <strong> and <button>)
const outputDiv = btn.parentElement.querySelector('div:not(:has(button))');
if (outputDiv) {
const text = outputDiv.textContent;
navigator.clipboard.writeText(text).then(() => {
btn.textContent = "Copied!";
setTimeout(() => btn.textContent = "Copy", 1000);
});
}
};
});
</script>
</body>
</html>
Key Takeaways
Symmetric encryption is simple and fast, but requires secure key distribution.
Diffie–Hellman solves the key distribution problem by letting two parties agree on a shared secret over an insecure channel.
Digital signatures add authenticity, ensuring that a message truly comes from the claimed sender.
Diffie–Hellman alone does not provide authentication — it only guarantees confidentiality. That’s why message signing is essential: it binds the exchanged public keys to a verified identity, preventing man‑in‑the‑middle attacks.
Combining these techniques gives you the pillars of secure communication: confidentiality, integrity, and authenticity.
Project Ideas Using Kaspa Encryption
Secure Login Sessions (Password + DH handshake)
Use symmetric encryption for storing session tokens.
Use Diffie–Hellman to establish a shared secret between client and server.
Sign session messages with the wallet’s private key to prove identity.
Demo: “Login with Kaspa Wallet” where the session is cryptographically bound to the wallet keys.
Encrypted Messaging App (Kaspa as relay; see Kasia.fyi)
Two peers exchange public keys via Kaspa transactions.
They establish a DH shared secret.
Messages are encrypted symmetrically and relayed through Kaspa.
Signatures ensure authenticity so you know the sender is who they claim.
Proof-of-Ownership Service (see KasStamp)
Use Kaspa wallet keys to sign arbitrary data (like a document hash).
Store the signed proof on Kaspa.
Anyone can verify the signature against the public key.
Demo: “Prove this file belongs to me” without revealing the file itself.
Secure Multiplayer Game State Sync
Players exchange DH keys to establish shared secrets.
Game moves are encrypted symmetrically.
Signatures prevent spoofed moves.
Kaspa acts as a relay or audit log for fairness.
Confidential Voting / Polling Demo
Each voter encrypts their vote with a shared secret.
Votes are signed with wallet keys to prove eligibility.
Results are decrypted only by authorized parties.
Demo: “On-chain poll with private votes but verifiable signatures.”
