|
const opensslPrefix = new TextEncoder().encode("Salted__"); |
|
|
|
async function encryptBuffer(data: Uint8Array, password: string) { |
|
const salt = crypto.getRandomValues(new Uint8Array(8)); |
|
const { iv, key } = await passwordToKeyAndIV(password, salt); |
|
|
|
const encrypted = await crypto.subtle.encrypt( |
|
{ name: "AES-CBC", iv: iv }, |
|
key, |
|
data |
|
); |
|
|
|
const encryptedArray = new Uint8Array(encrypted); |
|
const buffer = new Uint8Array( |
|
opensslPrefix.length + salt.length + encryptedArray.length |
|
); |
|
buffer.set(opensslPrefix, 0); |
|
buffer.set(salt, opensslPrefix.length); |
|
buffer.set(encryptedArray, opensslPrefix.length + salt.length); |
|
|
|
return encodeBase64(buffer); |
|
} |
|
|
|
async function decryptBuffer(workingBuffer: Uint8Array, password: string) { |
|
const encryptedBuffer = workingBuffer.slice(opensslPrefix.length); |
|
const salt = encryptedBuffer.slice(0, 8); |
|
const encrypted = encryptedBuffer.slice(8); |
|
|
|
const { key, iv } = await passwordToKeyAndIV(password, salt); |
|
|
|
const decrypted = await crypto.subtle.decrypt( |
|
{ name: "AES-CBC", iv: iv }, |
|
key, |
|
encrypted |
|
); |
|
|
|
return new TextDecoder().decode(decrypted); |
|
} |
|
|
|
async function passwordToKeyAndIV(password: string, salt: Uint8Array) { |
|
const baseKey = await crypto.subtle.importKey( |
|
"raw", |
|
new TextEncoder().encode(password), |
|
{ name: "PBKDF2" }, |
|
false, |
|
["deriveBits"] |
|
); |
|
|
|
// AES-256-CBC which requires a 256-bit key (32 bytes) + 128-bit IV (16 bytes) |
|
const keyLength = 32 * 8; // 32 bytes for AES-256 key |
|
const ivLength = 16 * 8; // 16 bytes for AES IV |
|
const totalLength = keyLength + ivLength; // Total bits needed |
|
|
|
const derivedBits = await crypto.subtle.deriveBits( |
|
{ |
|
name: "PBKDF2", |
|
salt: salt, |
|
iterations: 100_000, |
|
hash: "SHA-256", |
|
}, |
|
baseKey, |
|
totalLength |
|
); |
|
|
|
const keyBuffer = derivedBits.slice(0, 32); // First 32 bytes for the key |
|
const ivBuffer = derivedBits.slice(32, 48); // Next 16 bytes for the IV |
|
|
|
const key = await crypto.subtle.importKey( |
|
"raw", |
|
keyBuffer, |
|
{ name: "AES-CBC", length: 256 }, |
|
false, |
|
["encrypt", "decrypt"] |
|
); |
|
|
|
return { key, iv: new Uint8Array(ivBuffer) }; |
|
} |
|
|
|
async function encryptText(plaintext: string, password: string) { |
|
const encodedText = new TextEncoder().encode(plaintext); |
|
return encryptBuffer(encodedText, password); |
|
} |
|
|
|
async function decryptText(encrypted: string, password: string) { |
|
return decryptBuffer(decodeBase64(encrypted), password); |
|
} |
|
|
|
function encodeBase64(bytes: Uint8Array): string { |
|
return btoa(String.fromCharCode.apply(null, Array.from(bytes))); |
|
} |
|
|
|
function decodeBase64(base64String: string): Uint8Array { |
|
return Uint8Array.from(atob(base64String), (c) => c.charCodeAt(0)); |
|
} |