import process from '#node:process'
import crypto from 'hypercore-crypto'
import sodium from 'sodium-native'
import b4a from 'b4a'
import { join } from '#node:path'
import { SecureStorage } from './secure-storage.js'
/**
* IdentityManager — Persistent identity keypairs + mnemonic proof chains
*
* SECURITY: Private keys and mnemonics are stored encrypted on the filesystem
* using Argon2id + XSalsa20-Poly1305 (via SecureStorage). They are NEVER
* written to Hyperbee or any replicable storage.
*
* Storage layout:
* {storagePath}/keypair.enc — encrypted Ed25519 keypair (Argon2id vault)
* {storagePath}/mnemonic.enc — encrypted BIP39 mnemonic (Argon2id vault)
*
* Passphrase sources (in priority order):
* 1. opts.passphrase (Buffer or string)
* 2. process.env.HYPER_IDENTITY_PASSPHRASE
* 3. null → falls back to legacy Hyperbee storage (migration path)
*/
export class IdentityManager {
constructor (store, opts = {}) {
this._store = store // Hyperbee (for legacy migration reads only)
this._storagePath = opts.storagePath || null
this._passphrase = opts.passphrase
? (typeof opts.passphrase === 'string' ? Buffer.from(opts.passphrase) : opts.passphrase)
: (process.env.HYPER_IDENTITY_PASSPHRASE
? Buffer.from(process.env.HYPER_IDENTITY_PASSPHRASE)
: null)
this._keypair = null
this._mnemonic = null
this._identityKey = null
this._IdentityKey = opts.IdentityKey || null
this._encrypted = false // Whether using encrypted storage
}
/**
* Initialize — load or generate persistent keypair
*/
async ready () {
// Try encrypted storage first (if storagePath and passphrase are available)
if (this._storagePath && this._passphrase) {
await this._loadEncrypted()
return
}
// Fall back to legacy Hyperbee storage (migration path)
await this._loadLegacy()
}
/**
* Load keys from encrypted vaults
*/
async _loadEncrypted () {
this._encrypted = true
const keypairVault = new SecureStorage(join(this._storagePath, 'keypair.enc'))
const mnemonicVault = new SecureStorage(join(this._storagePath, 'mnemonic.enc'))
// Load or generate keypair
if (keypairVault.exists()) {
const plain = await keypairVault.decrypt(this._passphrase)
const data = JSON.parse(plain.toString())
this._keypair = {
publicKey: b4a.from(data.publicKey, 'base64'),
secretKey: b4a.from(data.secretKey, 'base64')
}
sodium.sodium_memzero(plain)
} else {
this._keypair = crypto.keyPair()
const plain = Buffer.from(JSON.stringify({
publicKey: b4a.toString(this._keypair.publicKey, 'base64'),
secretKey: b4a.toString(this._keypair.secretKey, 'base64')
}))
await keypairVault.encrypt(plain, this._passphrase)
sodium.sodium_memzero(plain)
}
// Load mnemonic if vault exists
if (mnemonicVault.exists()) {
const plain = await mnemonicVault.decrypt(this._passphrase)
this._mnemonic = plain.toString()
sodium.sodium_memzero(plain)
await this._loadIdentityKey()
}
// Migrate from legacy Hyperbee if vaults didn't exist but Hyperbee has keys
if (!keypairVault.exists()) {
const existing = await this._store.get('instance-keypair')
if (existing) {
// Migrate: encrypt the Hyperbee keypair into vault, then delete from Hyperbee
console.log('[IdentityManager] Migrating keypair from Hyperbee to encrypted vault')
const val = existing.value || existing
const encoding = (val.publicKey && val.publicKey.length === 44) ? 'base64' : 'hex'
this._keypair = {
publicKey: b4a.from(val.publicKey, encoding),
secretKey: b4a.from(val.secretKey, encoding)
}
const plain = Buffer.from(JSON.stringify({
publicKey: b4a.toString(this._keypair.publicKey, 'base64'),
secretKey: b4a.toString(this._keypair.secretKey, 'base64')
}))
await keypairVault.encrypt(plain, this._passphrase)
sodium.sodium_memzero(plain)
// Delete from Hyperbee (no longer needed)
await this._store.del('instance-keypair')
}
}
}
/**
* Load keys from legacy Hyperbee storage (unencrypted)
*/
async _loadLegacy () {
this._encrypted = false
const existing = await this._store.get('instance-keypair')
if (existing) {
const val = existing.value || existing
const encoding = (val.publicKey && val.publicKey.length === 44) ? 'base64' : 'hex'
this._keypair = {
publicKey: b4a.from(val.publicKey, encoding),
secretKey: b4a.from(val.secretKey, encoding)
}
} else {
this._keypair = crypto.keyPair()
await this._store.put('instance-keypair', {
publicKey: b4a.toString(this._keypair.publicKey, 'base64'),
secretKey: b4a.toString(this._keypair.secretKey, 'base64')
})
}
const mnemonicEntry = await this._store.get('identity-mnemonic')
if (mnemonicEntry) {
this._mnemonic = mnemonicEntry.value
await this._loadIdentityKey()
}
}
async _loadIdentityKey () {
if (!this._IdentityKey || !this._mnemonic) return
try {
this._identityKey = this._IdentityKey.from({ mnemonic: this._mnemonic })
} catch {
this._identityKey = null
}
}
getPublicKeyHex () {
if (!this._keypair) return null
return b4a.toString(this._keypair.publicKey, 'hex')
}
getPublicKey () {
return this._keypair ? this._keypair.publicKey : null
}
getKeypair () {
return this._keypair
}
/**
* Set mnemonic and derive identity key
* @param {string} mnemonic - BIP39 mnemonic
*/
async setMnemonic (mnemonic) {
this._mnemonic = mnemonic
if (this._encrypted && this._storagePath && this._passphrase) {
// Store encrypted
const vault = new SecureStorage(join(this._storagePath, 'mnemonic.enc'))
const plain = Buffer.from(mnemonic)
await vault.encrypt(plain, this._passphrase)
sodium.sodium_memzero(plain)
} else {
// Legacy: store in Hyperbee (unencrypted)
await this._store.put('identity-mnemonic', mnemonic)
}
await this._loadIdentityKey()
}
getMnemonic () {
return this._mnemonic
}
getIdentityKey () {
return this._identityKey
}
async bootstrap (devicePublicKey) {
if (!this._identityKey) throw new Error('No identity key loaded')
return this._identityKey.bootstrap(devicePublicKey)
}
sign (data) {
if (!this._keypair) throw new Error('Not initialized')
return crypto.sign(data, this._keypair.secretKey)
}
verify (data, signature) {
if (!this._keypair) throw new Error('Not initialized')
return crypto.verify(data, signature, this._keypair.publicKey)
}
getPublicKeyBase64 () {
if (!this._keypair) return null
return b4a.toString(this._keypair.publicKey, 'base64')
}
getKeypairString () {
if (!this._keypair) return null
return b4a.toString(this._keypair.publicKey, 'base64') +
b4a.toString(this._keypair.secretKey, 'base64')
}
getIdentityPublicKey () {
if (!this._identityKey) return null
return this._identityKey.publicKey
}
getIdentityPublicKeyHex () {
const pk = this.getIdentityPublicKey()
return pk ? b4a.toString(pk, 'hex') : null
}
/**
* Whether using encrypted storage
*/
get isEncrypted () {
return this._encrypted
}
/**
* Zero sensitive key material from memory
*/
destroy () {
if (this._keypair && this._keypair.secretKey) {
sodium.sodium_memzero(this._keypair.secretKey)
}
if (this._identityKey && typeof this._identityKey.clear === 'function') {
this._identityKey.clear()
}
if (this._passphrase) {
sodium.sodium_memzero(this._passphrase)
}
this._keypair = null
this._identityKey = null
this._mnemonic = null
this._passphrase = null
}
}
export default IdentityManager