128 lines
3.9 KiB
JavaScript
128 lines
3.9 KiB
JavaScript
/**
|
|
* E2E Encryption Service using TweetNaCl
|
|
* https://tweetnacl.js.org/
|
|
*/
|
|
|
|
const STORAGE_KEY = 'dgray_keypair';
|
|
|
|
class CryptoService {
|
|
constructor() {
|
|
this.nacl = null;
|
|
this.naclUtil = null;
|
|
this.keyPair = null;
|
|
this.ready = this.init();
|
|
}
|
|
|
|
async init() {
|
|
// Dynamically import TweetNaCl from CDN
|
|
if (!window.nacl) {
|
|
await this.loadScript('https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl-fast.min.js');
|
|
await this.loadScript('https://cdn.jsdelivr.net/npm/tweetnacl-util@0.15.1/nacl-util.min.js');
|
|
}
|
|
|
|
this.nacl = window.nacl;
|
|
this.naclUtil = window.nacl.util;
|
|
|
|
this.loadOrCreateKeyPair();
|
|
}
|
|
|
|
loadScript(src) {
|
|
return new Promise((resolve, reject) => {
|
|
if (document.querySelector(`script[src="${src}"]`)) {
|
|
resolve();
|
|
return;
|
|
}
|
|
const script = document.createElement('script');
|
|
script.src = src;
|
|
script.onload = resolve;
|
|
script.onerror = reject;
|
|
document.head.appendChild(script);
|
|
});
|
|
}
|
|
|
|
loadOrCreateKeyPair() {
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
|
|
if (stored) {
|
|
try {
|
|
const parsed = JSON.parse(stored);
|
|
this.keyPair = {
|
|
publicKey: this.naclUtil.decodeBase64(parsed.publicKey),
|
|
secretKey: this.naclUtil.decodeBase64(parsed.secretKey)
|
|
};
|
|
return;
|
|
} catch (e) {
|
|
console.warn('Failed to load keypair, generating new one');
|
|
}
|
|
}
|
|
|
|
this.generateKeyPair();
|
|
}
|
|
|
|
generateKeyPair() {
|
|
this.keyPair = this.nacl.box.keyPair();
|
|
|
|
const toStore = {
|
|
publicKey: this.naclUtil.encodeBase64(this.keyPair.publicKey),
|
|
secretKey: this.naclUtil.encodeBase64(this.keyPair.secretKey)
|
|
};
|
|
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
|
|
}
|
|
|
|
getPublicKey() {
|
|
if (!this.keyPair) return null;
|
|
return this.naclUtil.encodeBase64(this.keyPair.publicKey);
|
|
}
|
|
|
|
/**
|
|
* Encrypt a message for a recipient
|
|
* @param {string} message - Plain text message
|
|
* @param {string} recipientPublicKey - Base64 encoded public key
|
|
* @returns {object} - { nonce, ciphertext } both base64 encoded
|
|
*/
|
|
encrypt(message, recipientPublicKey) {
|
|
const nonce = this.nacl.randomBytes(this.nacl.box.nonceLength);
|
|
const messageUint8 = this.naclUtil.decodeUTF8(message);
|
|
const recipientKey = this.naclUtil.decodeBase64(recipientPublicKey);
|
|
|
|
const encrypted = this.nacl.box(
|
|
messageUint8,
|
|
nonce,
|
|
recipientKey,
|
|
this.keyPair.secretKey
|
|
);
|
|
|
|
return {
|
|
nonce: this.naclUtil.encodeBase64(nonce),
|
|
ciphertext: this.naclUtil.encodeBase64(encrypted)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Decrypt a message from a sender
|
|
* @param {string} ciphertext - Base64 encoded ciphertext
|
|
* @param {string} nonce - Base64 encoded nonce
|
|
* @param {string} senderPublicKey - Base64 encoded public key
|
|
* @returns {string|null} - Decrypted message or null if failed
|
|
*/
|
|
decrypt(ciphertext, nonce, senderPublicKey) {
|
|
try {
|
|
const decrypted = this.nacl.box.open(
|
|
this.naclUtil.decodeBase64(ciphertext),
|
|
this.naclUtil.decodeBase64(nonce),
|
|
this.naclUtil.decodeBase64(senderPublicKey),
|
|
this.keyPair.secretKey
|
|
);
|
|
|
|
if (!decrypted) return null;
|
|
return this.naclUtil.encodeUTF8(decrypted);
|
|
} catch (e) {
|
|
console.error('Decryption failed:', e);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const cryptoService = new CryptoService();
|