-
-
Save Einenlum/ccb4b167651a5f5954914fb810475f5e to your computer and use it in GitHub Desktop.
First approach to client-side encryption: DEK + Encryption/Decryption
This file contains 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; | |
} | |
async function generateDek( | |
password: string, | |
salt: Uint8Array, | |
): Promise<CryptoKey> { | |
const keyMaterial = await crypto.subtle.importKey( | |
'raw', | |
new TextEncoder().encode(password), | |
{ name: 'PBKDF2' }, | |
false, | |
['deriveKey'], | |
); | |
const dek = await crypto.subtle.deriveKey( | |
{ | |
name: 'PBKDF2', | |
salt: salt, | |
iterations: 100_000, | |
hash: 'SHA-256', | |
}, | |
keyMaterial, | |
{ name: 'AES-GCM', length: 256 }, | |
true, | |
['encrypt', 'decrypt'], | |
); | |
return dek; | |
} | |
async function uint8arrayToCryptoKey( | |
keyValue: Uint8Array, | |
exportable: boolean = true, | |
): Promise<CryptoKey> { | |
return await crypto.subtle.importKey( | |
'raw', | |
keyValue, | |
{ name: 'AES-GCM' }, | |
exportable, | |
['encrypt', 'decrypt'], | |
); | |
} | |
async function base64ToCryptoKey( | |
keyString: Base64String, | |
exportable: boolean = true, | |
): Promise<CryptoKey> { | |
const raw = base64ToUint8Array(keyString); | |
return await uint8arrayToCryptoKey(raw, exportable); | |
} | |
async function encryptData( | |
key: CryptoKey, | |
data: any, | |
): Promise<EncryptionResult> { | |
const iv = generateIv(); | |
const serializedData = JSON.stringify(data); | |
const encryptedData = 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 = await crypto.subtle.decrypt( | |
{ name: 'AES-GCM', iv: base64ToUint8Array(iv) }, | |
key, | |
base64ToUint8Array(encryptedData), | |
); | |
const decoded = new TextDecoder().decode(decryptedData); | |
return JSON.parse(decoded); | |
} |
This file contains 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 = '...'; | |
const salt = generateSalt(); | |
const dek = await generateDek(userPassword, salt); | |
// Store this dek CryptoKey in your JS store (with React, Vue, Svelte, AlpinejS...) | |
// 'bduYb/cMqAXwd3muWT4lng==' | |
const saltString = uint8ArrayToBase64(salt); | |
// 'WGObg9QpgtfaJ3Dk9hg1x2tnHq/kKQnLfUxvW7SS5vA=' | |
const dekBase64 = await cryptoKeyToBase64(dek); | |
localStorage.setItem('dek', dekBase64); | |
// set the saltString to the registration form and submit | |
// ... | |
/* Login of a user */ | |
// After login the backend sends the dek_salt | |
const saltString = response.data.dek_salt; | |
const salt = base64ToUint8Array(saltString); | |
const dek = await generateDekFromPassword(userPassword, salt); | |
// store the dek CryptoKey in the JS store | |
// 'WGObg9QpgtfaJ3Dk9hg1x2tnHq/kKQnLfUxvW7SS5vA=' | |
const dekBase64 = await cryptoKeyToBase64(dek); | |
localStorage.setItem('dek', dekBase64); | |
/* On page refresh */ | |
const dekBase64 = localStorage.getItem('dek'); | |
if (dekBase64) { | |
const dek = await base64ToCryptoKey(dekBase64); | |
// store the dek CryptoKey in the JS store | |
} else { | |
// We logout the user and redirect them to the login page | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment