-
-
Save einenlum/3770fccafc11c2c97aee61722fcd12df to your computer and use it in GitHub Desktop.
Second approach to E2EE: DEK + KEK + Encryption/Decryption
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* 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 | |
| // ... |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for doing this work and your wonderful articles on einenlum.com.