Skip to content

Instantly share code, notes, and snippets.

@einenlum

einenlum/app.ts Secret

Last active October 24, 2025 05:12
Show Gist options
  • Select an option

  • Save einenlum/3770fccafc11c2c97aee61722fcd12df to your computer and use it in GitHub Desktop.

Select an option

Save einenlum/3770fccafc11c2c97aee61722fcd12df to your computer and use it in GitHub Desktop.
Second approach to E2EE: DEK + KEK + Encryption/Decryption
type Base64String = string;
type EncryptionResult = {
iv: Base64String;
encryptedData: Base64String;
};
function generateRandomKey(length: number): Uint8Array {
// This will return a secure random Uint8Array of `length` bytes
return crypto.getRandomValues(new Uint8Array(length));
}
function generateSalt(): Uint8Array {
return generateRandomKey(16);
}
function generateIv(): Uint8Array {
return generateRandomKey(12);
}
function uint8ArrayToBase64(binary: Uint8Array): Base64String {
return btoa(String.fromCharCode(...binary));
}
function base64ToUint8Array(base64: Base64String): Uint8Array {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function generateDekValue(): Uint8Array {
// This will return a secure random Uint8Array of 32 bytes (256 bits)
return generateRandomKey(32);
}
async function generateKek(
password: string,
salt: Uint8Array,
): Promise<CryptoKey> {
const keyMaterial: CryptoKey = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
{ name: 'PBKDF2' },
false,
['deriveKey'],
);
const kek: CryptoKey = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 600_000,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt'],
);
return kek;
}
async function cryptoKeyToBase64(key: CryptoKey): Promise<Base64String> {
const rawKey: ArrayBuffer = await crypto.subtle.exportKey('raw', key);
return uint8ArrayToBase64(new Uint8Array(rawKey));
}
async function uint8ArrayToCryptoKey(
rawKey: Uint8Array,
exportable: boolean = true,
): Promise<CryptoKey> {
return await crypto.subtle.importKey(
'raw',
rawKey,
{ name: 'AES-GCM' },
exportable,
['encrypt', 'decrypt'],
);
}
async function base64ToCryptoKey(
dekString: Base64String,
exportable: boolean = true,
): Promise<CryptoKey> {
const raw: Uint8Array = base64ToUint8Array(dekString);
return await uint8ArrayToCryptoKey(raw, exportable);
}
async function encryptData(
key: CryptoKey,
data: any,
): Promise<EncryptionResult> {
const iv: Uint8Array = generateIv();
const serializedData: string = JSON.stringify(data);
const encryptedData: ArrayBuffer = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
new TextEncoder().encode(serializedData),
);
return {
iv: uint8ArrayToBase64(iv),
encryptedData: uint8ArrayToBase64(new Uint8Array(encryptedData)),
};
}
async function decryptData(
key: CryptoKey,
iv: Base64String,
encryptedData: Base64String,
): Promise<any> {
const decryptedData: ArrayBuffer = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: base64ToUint8Array(iv) },
key,
base64ToUint8Array(encryptedData),
);
const decoded: string = new TextDecoder().decode(decryptedData);
return JSON.parse(decoded);
}
/* Registration of a user */
const userPassword = '...';
// Generation of the KEK
const kekSalt: Uint8Array = generateSalt();
const kekSaltAsBase64: Base64String = uint8ArrayToBase64(kekSalt);
const kek: CryptoKey = await generateKek(userPassword, kekSalt);
// DEK creation
const dek: CryptoKey = await uint8ArrayToCryptoKey(generateDekValue());
// DEK Storage
// Store the dek CryptoKey in your JS store (with React, Vue, Svelte, AlpinejS...)
// ...
// And its base64 representation to localStorage
const dekAsBase64: Base64String = await cryptoKeyToBase64(dek);
window.localStorage.setItem('dek', dekAsBase64);
// encryption of the DEK
const { iv, encryptedData } = await encryptData(kek, dek);
// Send registration form
await axios.post('/register', {
email: '...',
password: userPassword,
kek_salt: kekSaltAsBase64,
dek_iv: iv,
encrypted_dek: encryptedData,
});
/* Login of a user */
// clear-text password the user entered in the login form
const userPassword: string = '...';
// login call
// const response = await axios.post('/login', {...});
const { kek_salt, dek_iv, encrypted_dek } = response.data;
const kek: CryptoKey = await generateKek(
userPassword,
base64ToUint8Array(kek_salt)
);
// We get the exported DEK as base64
const dekAsBase64: Base64String = await decryptData(kek, dek_iv, encrypted_dek);
window.localStorage.setItem('dek', dekAsBase64);
// We convert it back to a CryptoKey
const dek: CryptoKey = await base64ToCryptoKey(dekAsBase64);
// And store it in our JS store
// ...
/* Changing the user's password */
// Data sent from the backend when the form appears
const oldKekSalt: Base64String = '...';
const oldDekIv: Base64String = '...';
const oldEncryptedDek: Base64String = '...';
// Taken from the password form
const oldPassword: string = '...';
const newPassword: string = '...';
const oldKek: CryptoKey = await generateKek(
oldPassword,
base64ToUint8Array(oldKekSalt)
);
const dekAsBase64: Base64String = await decryptData(
oldKek,
oldDekIv,
oldEncryptedDek,
);
const newKekSalt: Uint8Array = generateSalt();
const newKek: CryptoKey = await generateKek(
newPassword,
newKekSalt
);
const newKekSaltAsBase64: Base64String = uint8ArrayToBase64(newKekSalt);
const newEncryption: EncryptionResult = await encryptData(
newKek,
dekAsBase64
);
const newDekIv: Base64String = newEncryption.iv;
const newEncryptedDek: Base64String = newEncryption.encryptedData;
// Send these new data with the form:
await axios.post('/change-password', {
current_password: oldPassword,
new_password: newPassword,
kek_salt: newKekSaltAsBase64,
dek_iv: newDekIv,
encrypted_dek: newEncryptedDek,
});
// On success we can update the local storage and JS store
// ...
@tgallagher2017
Copy link

Thanks for doing this work and your wonderful articles on einenlum.com.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment