import sodium from 'sodium-native'
import b4a from 'b4a'
/**
* SecureStorage — Encrypted vault for sensitive data
*
* Uses Argon2id for key derivation and XSalsa20-Poly1305 for encryption.
* Stores encrypted data in a Hyperbee or similar key-value store.
*/
export class SecureStorage {
constructor (store) {
this._store = store // Hyperbee or similar key-value store
}
/**
* Derive encryption key from password using Argon2id
* @param {string} password - User password
* @param {Buffer} salt - 16-byte salt
* @returns {Buffer} 32-byte key
*/
static deriveKey (password, salt) {
const key = b4a.alloc(32)
const pw = b4a.from(password)
sodium.crypto_pwhash(
key,
pw,
salt,
sodium.crypto_pwhash_OPSLIMIT_MODERATE,
sodium.crypto_pwhash_MEMLIMIT_MODERATE,
sodium.crypto_pwhash_ALG_ARGON2ID13
)
return key
}
/**
* Encrypt data with key
* @param {Buffer} data - Data to encrypt
* @param {Buffer} key - 32-byte encryption key
* @returns {{ nonce: Buffer, ciphertext: Buffer }}
*/
static encrypt (data, key) {
const nonce = b4a.alloc(24)
sodium.randombytes_buf(nonce)
const ciphertext = b4a.alloc(data.length + sodium.crypto_secretbox_MACBYTES)
sodium.crypto_secretbox_easy(ciphertext, data, nonce, key)
return { nonce, ciphertext }
}
/**
* Decrypt data with key
* @param {Buffer} ciphertext - Encrypted data
* @param {Buffer} nonce - 24-byte nonce
* @param {Buffer} key - 32-byte decryption key
* @returns {Buffer} Decrypted data
*/
static decrypt (ciphertext, nonce, key) {
const plaintext = b4a.alloc(ciphertext.length - sodium.crypto_secretbox_MACBYTES)
const ok = sodium.crypto_secretbox_open_easy(plaintext, ciphertext, nonce, key)
if (!ok) throw new Error('Decryption failed')
return plaintext
}
/**
* Store encrypted value
* @param {string} name - Vault entry name
* @param {string} value - Value to encrypt
* @param {string} password - Encryption password
*/
async put (name, value, password) {
const salt = b4a.alloc(16)
sodium.randombytes_buf(salt)
const key = SecureStorage.deriveKey(password, salt)
const { nonce, ciphertext } = SecureStorage.encrypt(b4a.from(value), key)
await this._store.put(`vault:${name}`, {
salt: b4a.toString(salt, 'hex'),
nonce: b4a.toString(nonce, 'hex'),
ciphertext: b4a.toString(ciphertext, 'hex')
})
}
/**
* Retrieve and decrypt value
* @param {string} name - Vault entry name
* @param {string} password - Decryption password
* @returns {string|null} Decrypted value or null
*/
async get (name, password) {
const entry = await this._store.get(`vault:${name}`)
if (!entry) return null
const { salt, nonce, ciphertext } = entry.value
const key = SecureStorage.deriveKey(password, b4a.from(salt, 'hex'))
try {
const plaintext = SecureStorage.decrypt(
b4a.from(ciphertext, 'hex'),
b4a.from(nonce, 'hex'),
key
)
return b4a.toString(plaintext)
} catch {
return null
}
}
/**
* Check if vault entry exists
* @param {string} name - Vault entry name
* @returns {boolean}
*/
async has (name) {
const entry = await this._store.get(`vault:${name}`)
return entry !== null
}
/**
* Delete vault entry
* @param {string} name - Vault entry name
*/
async del (name) {
await this._store.del(`vault:${name}`)
}
}
export default SecureStorage