]> git.codecow.com Git - libnemo.git/commitdiff
Use different salt and IV when storing in safe. Encode entire object of buffers as...
authorChris Duncan <chris@zoso.dev>
Wed, 16 Jul 2025 14:20:10 +0000 (07:20 -0700)
committerChris Duncan <chris@zoso.dev>
Wed, 16 Jul 2025 14:20:10 +0000 (07:20 -0700)
src/lib/workers/safe.ts
src/types.d.ts

index 404a446f563f19cfabc98c6782f1959fc49e3bc6..ac194f0b12ad09f76f1376663271e0efd4d27762 100644 (file)
@@ -4,7 +4,7 @@
 'use strict'
 
 import { WorkerInterface } from './worker-interface'
-import { default as Convert, bytes } from '#src/lib/convert.js'
+import { default as Convert, base32, bytes } from '#src/lib/convert.js'
 import { Entropy } from '#src/lib/entropy.js'
 import { Data, Headers, SafeRecord } from '#types'
 
@@ -15,6 +15,8 @@ export class Safe extends WorkerInterface {
        static DB_NAME = 'libnemo'
        static STORE_NAME = 'Safe'
        static ERR_MSG = 'Failed to store item in Safe'
+       static #decoder: TextDecoder = new TextDecoder()
+       static #encoder: TextEncoder = new TextEncoder()
        static #storage: IDBDatabase
 
        static {
@@ -43,8 +45,7 @@ export class Safe extends WorkerInterface {
                                        result = `unknown Safe method ${method}`
                                }
                        }
-               } catch (err) {
-                       console.log(err)
+               } catch {
                        result = false
                }
                return result
@@ -56,7 +57,7 @@ export class Safe extends WorkerInterface {
        static async destroy (name: string): Promise<boolean> {
                try {
                        return await this.#delete(name)
-               } catch (err) {
+               } catch {
                        throw new Error(this.ERR_MSG)
                }
        }
@@ -67,42 +68,48 @@ export class Safe extends WorkerInterface {
        static async set (name: string, data: Data): Promise<boolean> {
                const { password } = data
                delete data.password
-               let passkey: CryptoKey
-               try {
-                       if (await this.#exists(name)) throw new Error('Record is already locked')
-                       passkey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
-               } catch {
-                       throw new Error(this.ERR_MSG)
-               } finally {
-                       bytes.erase(password)
-               }
-               if (this.#isInvalid(name, passkey, data)) {
-                       throw new Error(this.ERR_MSG)
-               }
 
                try {
-                       const iv = await Entropy.create()
+                       if (await this.#exists(name)) {
+                               throw null
+                       }
+                       const derivationKey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
+                       if (this.#isInvalid(name, derivationKey, data)) {
+                               throw null
+                       }
+
+                       const base32: { [key: string]: string } = {}
+                       for (const d of Object.keys(data)) {
+                               base32[d] = bytes.toBase32(new Uint8Array(data[d]))
+                       }
+                       const serialized = JSON.stringify(base32)
+                       const encoded = this.#encoder.encode(serialized)
+
+                       const salt = await Entropy.create()
                        const derivationAlgorithm: Pbkdf2Params = {
                                name: 'PBKDF2',
                                hash: 'SHA-512',
-                               salt: iv.bytes,
+                               salt: salt.bytes,
                                iterations: 210000
                        }
                        const derivedKeyType: AesKeyGenParams = {
                                name: 'AES-GCM',
                                length: 256
                        }
-                       passkey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, passkey, derivedKeyType, false, ['encrypt'])
-                       for (const d of Object.keys(data)) {
-                               data[d] = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, data[d])
-                       }
+                       const encryptionKey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, ['encrypt'])
+
+                       const iv = await Entropy.create()
+                       const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, encryptionKey, encoded)
                        const record: SafeRecord = {
                                iv: iv.hex,
-                               data
+                               salt: salt.hex,
+                               encrypted
                        }
                        return await this.#add(record, name)
                } catch (err) {
                        throw new Error(this.ERR_MSG)
+               } finally {
+                       bytes.erase(password)
                }
        }
 
@@ -112,50 +119,47 @@ export class Safe extends WorkerInterface {
        static async get (name: string, data: Data): Promise<Data | null> {
                const { password } = data
                delete data.password
-               let passkey: CryptoKey
-               try {
-                       passkey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
-               } catch {
-                       return null
-               } finally {
-                       bytes.erase(password)
-               }
-               if (this.#isInvalid(name, passkey)) {
-                       return null
-               }
 
-               let record: SafeRecord
                try {
-                       record = await this.#get(name)
-               } catch {
-                       return null
-               }
-               if (record == null) {
-                       return null
-               }
+                       const derivationKey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
+                       if (this.#isInvalid(name, derivationKey)) {
+                               throw null
+                       }
 
-               try {
-                       const iv = await Entropy.import(record.iv)
-                       const { data } = record
+                       const record: SafeRecord = await this.#get(name)
+                       if (record == null) {
+                               throw null
+                       }
+                       const { encrypted } = record
+
+                       const salt = await Entropy.import(record.salt)
                        const derivationAlgorithm: Pbkdf2Params = {
                                name: 'PBKDF2',
                                hash: 'SHA-512',
-                               salt: iv.bytes,
+                               salt: salt.bytes,
                                iterations: 210000
                        }
                        const derivedKeyType: AesKeyGenParams = {
                                name: 'AES-GCM',
                                length: 256
                        }
-                       passkey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, passkey, derivedKeyType, false, ['decrypt'])
-                       for (const d of Object.keys(data)) {
-                               const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, data[d])
-                               data[d] = decrypted
+                       const decryptionKey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, ['decrypt'])
+
+                       const iv = await Entropy.import(record.iv)
+                       const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, decryptionKey, encrypted)
+                       const decoded = this.#decoder.decode(decrypted)
+                       const deserialized: { [key: string]: string } = JSON.parse(decoded)
+
+                       const bytes: Data = {}
+                       for (const d of Object.keys(deserialized)) {
+                               bytes[d] = new Uint8Array(base32.toBytes(deserialized[d])).buffer
                        }
                        await this.destroy(name)
-                       return data
+                       return bytes
                } catch (err) {
                        return null
+               } finally {
+                       bytes.erase(password)
                }
        }
 
@@ -169,7 +173,7 @@ export class Safe extends WorkerInterface {
                if (typeof data === 'object') {
                        try {
                                JSON.stringify(data, (k, v) => typeof v === 'bigint' ? v.toString() : v)
-                       } catch (err) {
+                       } catch {
                                return true
                        }
                }
index 7a360aecd4234392085019887611bc1ae74f25da..4754eb3ae39fa466b172be1b758e47517664e17c 100644 (file)
@@ -17,7 +17,8 @@ export type KeyPair = {
 
 export type SafeRecord = {
        iv: string
-       data: Data
+       salt: string
+       encrypted: ArrayBuffer
 }
 
 export type UnknownNumber = number | unknown