Crypto.js
2024
Exploring the modern browser's native cryptography libraries for doing public/private key encryption.
See: home, chat, reflector, litmd, crypto,
Goals
- Generate keypair
- Encrypt a text message (usually a JSON string in our case).
- Decrypt the message
- Store keypair for later use (usually in localstorage, often via stree)
Exporting a derived symmetric key
webcryptobox, written in an eerily similar style as simpatico (minimal, zero-deps, no build, etc) It is small, build-less, written with modern ES6 and quite legible.
js
import * as wcb from './node_modules/webcryptobox/index.js';
const alice = await wcb.generateKeyPair()
const bob = await wcb.generateKeyPair()
const text = 'Test message'
const message = wcb.decodeText(text)
const box = await wcb.encryptTo({ message, privateKey: alice.privateKey, publicKey: bob.publicKey })
const hexBox = wcb.encodeHex(box);
const decryptedBox = await wcb.decryptFrom({ box: wcb.decodeHex(hexBox), privateKey: bob.privateKey, publicKey: alice.publicKey })
const decryptedText = wcb.encodeText(decryptedBox)
assertEquals(decryptedText, text);
// Now do key export and import with alice's keys
const alicePubExported = await wcb.exportPublicKeyPem(alice.publicKey);
const alicePrivExported = await wcb.exportPrivateKeyPem(alice.privateKey);
log({ alicePubExported, alicePrivExported });
const alicePubImported = await wcb.importPublicKeyPem(alicePubExported);
const alicePrivImported = await wcb.importPrivateKeyPem(alicePrivExported);
const alice2 = {publicKey: alicePubImported, privateKey: alicePrivImported};
const box2 = await wcb.encryptTo({message, privateKey: alice2.privateKey, publicKey: bob.publicKey});
const decryptedBox2 = await wcb.decryptFrom({box: box2, privateKey: bob.privateKey, publicKey: alice2.publicKey});
const decryptedText2 = wcb.encodeText(decryptedBox2);
// Get a printable sha256 hash of the key
const alicePubKeyFingerprint = await wcb.sha256Fingerprint(alice.publicKey);
log('alicePubKeyFingerprint',wcb.encodeBase64(alicePubKeyFingerprint));
assertEquals(decryptedText, decryptedText2);
RSA from scratch
Adapted from Implementing RSA from Scratch via hn.
js
// the browser doesn't need this but node does
// const BigInt = require('big-integer');
const gcd = (a, b) => extendedEuclidean(a,b)[0];
const lcm = (x, y) => (x * y) / gcd(x, y);
function extendedEuclidean(a, b) {
if (a === 0n) {
return [b, 0n, 1n];
}
let [gcd, x1, y1] = extendedEuclidean(b % a, a);
let x = y1 - (b / a) * x1;
let y = x1;
return [gcd, x, y];
}
function modularInverse(a, m) {
let [gcd, x, y] = extendedEuclidean(a, m);
if (gcd !== 1n) {
return null;
} else {
x = (x % m + m) % m;
return x;
}
}
function modularExponentiation(base, exponent, modulus) {
if (modulus === 1n) return 0n;
let result = 1n;
base = base % modulus;
while (exponent > 0) {
if (exponent % 2n === 1) {
result = (result * base) % modulus;
}
exponent = exponent / 2n;
base = (base * base) % modulus;
}
return result;
}
function generateRSAKeys(p, q) {
let n = p * q;
let lambdaN = lcm(p - 1n, q - 1n);
let e = 65537n;
let d = extendedEuclidean(e, lambdaN)[0];
if (d < 0) d = d + lambdaN;
return { publicKey: { e, n }, privateKey: { d, n } };
}
function encrypt(message, publicKey) {
return modularExponentiation(message, publicKey.e, publicKey.n);
}
function decrypt(ciphertext, privateKey) {
return modularExponentiation(ciphertext, privateKey.d, privateKey.n);
}
// Testing
let keys = generateRSAKeys(31337n, 31357n);
let publicKey = keys.publicKey;
let privateKey = keys.privateKey;
let message = 80087n;
let encrypted = encrypt(message, publicKey);
let decrypted = decrypt(encrypted, privateKey);
console.log(decrypted)
Discussion
Why crypto? Because you should encrypt all data in transit and at rest because of exploits and because of privacy.
References
- MDN's generateKey() docs
- MDN's exportKey() docs
- MDN's importKey() docs
- See the w3c web crypto spec
- Crypto 101 is a good intro text to the field.
Copyright SimpatiCorp 2024