Skip to content

Instantly share code, notes, and snippets.

@Einenlum

Einenlum/app.ts Secret

Last active January 22, 2025 14:13
Show Gist options
  • Save Einenlum/ccb4b167651a5f5954914fb810475f5e to your computer and use it in GitHub Desktop.
Save Einenlum/ccb4b167651a5f5954914fb810475f5e to your computer and use it in GitHub Desktop.
First approach to client-side encryption: DEK + 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;
}
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);
}
/* 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