OverviewFilesCommitsIssuesDocsReleasesPeers

hyperident / lib / identity-manager.js

1103 lines · javascript ref:
/**
 * Identity Manager - Core identity operations for Hyperident
 * Handles mnemonic generation, storage, and profile management
 */

import IdentityKey from 'keet-identity-key'
import { CryptoManager } from './crypto.js'
import { FileManager } from './file-ops.js'
import path from 'path'
import fs from 'fs'

/**
 * IdentityManager class for managing hyperident identities and profiles
 */
export class IdentityManager {
  /**
   * Generate a new identity with optional passphrase protection and device chain
   * @param {string|null} passphrase - Optional passphrase for encryption
   * @param {string} deviceName - Name for this device (default: 'default')
   * @returns {Promise<Object>} Generated identity data
   */
  static async generateIdentity(passphrase = null, deviceName = 'default') {
    try {
      // Generate mnemonic using keet-identity-key
      const mnemonic = IdentityKey.generateMnemonic()
      
      // Create IdentityKey instance from mnemonic
      const identityKey = await IdentityKey.from({ mnemonic })
      
    // Generate device keypair (compatible with keet-identity-key)
    const deviceKeyPair = await CryptoManager.generateRawDeviceKeyPair()
    
    // Create device proof using keet-identity-key
    const deviceProof = await identityKey.bootstrap(deviceKeyPair.publicKey)
      
      // Ensure proof is a Buffer
      const proofBuffer = Buffer.isBuffer(deviceProof) ? deviceProof : Buffer.from(deviceProof)
      
      // Create identity object
      const identity = {
        mnemonic,
        publicKey: identityKey.identityKeyPair.publicKey.toString('hex'),
        version: '1.0.0',
        created: new Date().toISOString(),
        algorithm: 'keet-identity-key',
        deviceChain: true
      }
      
      // Save the identity
      await this.saveIdentity(mnemonic, passphrase)
      
      // Save device chain
      await this.saveDeviceChain(deviceKeyPair, proofBuffer, deviceName)
      
      return identity
    } catch (error) {
      throw new Error(`Failed to generate identity: ${error.message}`)
    }
  }

  /**
   * Save identity to ~/.hyperident/ directory
   * @param {string} mnemonic - The mnemonic to save
   * @param {string|null} passphrase - Optional passphrase for encryption
   * @returns {Promise<void>}
   */
  static async saveIdentity(mnemonic, passphrase = null) {
    try {
      // Ensure hyperident directory exists
      await FileManager.ensureHyperidentDir()
      
      // Create IdentityKey instance for public key storage
      const identityKey = await IdentityKey.from({ mnemonic })
      
      // Create identity data structure
      const identityData = {
        version: '1.0.0',
        created: new Date().toISOString(),
        algorithm: 'keet-identity-key',
        encrypted: !!passphrase
      }
      
      if (passphrase) {
        // argon2id + XSalsa20-Poly1305 (hyper-social #47). The KDF cost is RECORDED in the vault, not
        // assumed at read time, so it can be raised later without stranding vaults written today.
        const salt = CryptoManager.generateSalt()
        const params = CryptoManager.kdfParams()
        const box = CryptoManager.encryptSecret(Buffer.from(mnemonic, 'utf8'), passphrase, salt, params)

        identityData.mnemonic = box.toString('hex')
        identityData.salt = salt.toString('hex')
        identityData.kdf = params.kdf
        identityData.opslimit = params.opslimit
        identityData.memlimit = params.memlimit
      } else {
        // Store mnemonic in plain text
        identityData.mnemonic = mnemonic
      }
      
      // Save identity file
      await FileManager.writeIdentityFile(identityData, identityData.encrypted)
      
      // Save public key file
      await FileManager.writePublicKey(identityKey.identityKeyPair.publicKey.toString('hex'))
      
      // Initialize empty profiles.json if it doesn't exist
      try {
        await FileManager.readProfiles()
      } catch (error) {
        // File doesn't exist, create empty profiles
        const emptyProfiles = {
          version: '1.0.0',
          profiles: {}
        }
        await FileManager.writeProfiles(emptyProfiles)
      }
      
    } catch (error) {
      throw new Error(`Failed to save identity: ${error.message}`)
    }
  }

  /**
   * Load identity from ~/.hyperident/ directory
   * @param {string|null} passphrase - Optional passphrase for decryption
   * @returns {Promise<Object>} Loaded identity data
   */
  static async loadIdentity(passphrase = null) {
    try {
      // Read identity file
      const identityData = await FileManager.readIdentityFile()
      
      let mnemonic
      if (identityData.encrypted) {
        if (!passphrase) {
          throw new Error('Identity is encrypted but no passphrase provided')
        }
        
        // A vault with no `kdf` field predates the argon2id/secretbox fix (#47) and is XOR-"encrypted".
        // Say so plainly instead of reporting "Invalid passphrase": the passphrase is not the problem,
        // and that secret should be treated as exposed and re-minted, not merely re-encrypted.
        if (!identityData.kdf) {
          throw new Error(
            'This vault was written with the pre-#47 XOR scheme, which does not protect it. ' +
            'Recover the mnemonic with an older hyperident, then re-create the vault. ' +
            'Treat the secret as exposed if the file ever left this machine.'
          )
        }
        const salt = Buffer.from(identityData.salt, 'hex')
        const params = {
          ...CryptoManager.kdfParams(),
          // Honour the cost the vault was WRITTEN with — using today's default would derive a
          // different key and read as a wrong passphrase.
          opslimit: identityData.opslimit || CryptoManager.kdfParams().opslimit,
          memlimit: identityData.memlimit || CryptoManager.kdfParams().memlimit
        }
        try {
          const box = Buffer.from(identityData.mnemonic, 'hex')
          mnemonic = CryptoManager.decryptSecret(box, passphrase, salt, params).toString('utf8')

          // Validate that the decrypted mnemonic is valid by trying to create an IdentityKey
          await IdentityKey.from({ mnemonic })
        } catch (error) {
          throw new Error('Invalid passphrase')
        }
      } else {
        if (passphrase) {
          throw new Error('Identity is not encrypted but passphrase provided')
        }
        mnemonic = identityData.mnemonic
      }
      
      // Create IdentityKey instance from mnemonic
      const identityKey = await IdentityKey.from({ mnemonic })
      
      // Return complete identity object
      return {
        mnemonic,
        publicKey: identityKey.identityKeyPair.publicKey.toString('hex'),
        version: identityData.version,
        created: identityData.created,
        algorithm: identityData.algorithm,
        encrypted: identityData.encrypted
      }
      
    } catch (error) {
      if (error.code === 'ENOENT') {
        throw new Error('Identity not found. Run "hyperident init" to create one.')
      }
      throw new Error(`Failed to load identity: ${error.message}`)
    }
  }

  /**
   * Get the public key from the identity
   * @returns {Promise<string>} Public key as hex string
   */
  static async getPublicKey() {
    try {
      // Try to read from public key file first (faster)
      return await FileManager.readPublicKey()
    } catch (error) {
      // Fall back to reading from identity file
      try {
        const identityData = await FileManager.readIdentityFile()
        if (identityData.encrypted) {
          throw new Error('Identity is encrypted. Cannot extract public key without passphrase.')
        }
        
        const identityKey = await IdentityKey.from({ mnemonic: identityData.mnemonic })
        return identityKey.identityKeyPair.publicKey.toString('hex')
      } catch (identityError) {
        throw new Error('Identity not found. Run "hyperident init" to create one.')
      }
    }
  }

  /**
   * Check if identity files exist
   * @returns {boolean} True if identity exists
   */
  static identityExists() {
    try {
      const hyperidentPath = FileManager.getHyperidentPath()
      const identityPath = path.join(hyperidentPath, 'identity')
      
      // Check if identity file exists using synchronous method for simplicity
      return fs.existsSync(identityPath)
    } catch (error) {
      return false
    }
  }

  /**
   * Create a new profile
   * @param {string} name - Profile name
   * @param {string} description - Optional profile description
   * @returns {Promise<void>}
   */
  static async createProfile(name, description = '') {
    try {
      // Validate profile name
      if (!this.validateProfileName(name)) {
        throw new Error('Invalid profile name. Must be 1-32 characters, alphanumeric and hyphens only.')
      }

      // Check if identity exists
      if (!this.identityExists()) {
        throw new Error('Identity not found. Run "hyperident init" to create one.')
      }

      // Load existing profiles
      let profilesData
      try {
        profilesData = await FileManager.readProfiles()
      } catch (error) {
        // Initialize empty profiles if file doesn't exist
        profilesData = {
          version: '1.0.0',
          profiles: {}
        }
      }

      // Check if profile already exists
      if (profilesData.profiles[name]) {
        throw new Error(`Profile '${name}' already exists`)
      }

      // Create new profile first
      const now = new Date().toISOString()
      profilesData.profiles[name] = {
        name,
        description,
        created: now,
        lastModified: now
      }

      // Save updated profiles first
      await FileManager.writeProfiles(profilesData)

      // Now generate and cache address
      try {
        const address = await this.getProfileAddress(name)
        await this.updateProfileAddress(name, address)
      } catch (error) {
        // If address generation fails, we still have the profile created
        // Address will be generated on first access
        console.warn(`Warning: Could not cache address during profile creation: ${error.message}`)
      }

    } catch (error) {
      throw new Error(`Failed to create profile: ${error.message}`)
    }
  }

  /**
   * Delete a profile
   * @param {string} name - Profile name to delete
   * @returns {Promise<void>}
   */
  static async deleteProfile(name) {
    try {
      // Check if identity exists
      if (!this.identityExists()) {
        throw new Error('Identity not found. Run "hyperident init" to create one.')
      }

      // Load existing profiles
      const profilesData = await FileManager.readProfiles()

      // Check if profile exists
      if (!profilesData.profiles[name]) {
        throw new Error(`Profile '${name}' not found`)
      }

      // Delete the profile
      delete profilesData.profiles[name]

      // Save updated profiles
      await FileManager.writeProfiles(profilesData)

    } catch (error) {
      if (error.code === 'ENOENT') {
        throw new Error('No profiles found. Create a profile first with "hyperident create-profile".')
      }
      throw new Error(`Failed to delete profile: ${error.message}`)
    }
  }

  /**
   * List all profiles
   * @returns {Promise<Object>} Profiles data
   */
  static async listProfiles() {
    try {
      // Check if identity exists
      if (!this.identityExists()) {
        throw new Error('Identity not found. Run "hyperident init" to create one.')
      }

      // Load profiles
      const profilesData = await FileManager.readProfiles()
      
      // Sort profiles alphabetically by name for consistent output
      const sortedProfiles = {}
      const sortedNames = Object.keys(profilesData.profiles).sort()
      
      for (const name of sortedNames) {
        sortedProfiles[name] = profilesData.profiles[name]
      }

      return {
        version: profilesData.version,
        profiles: sortedProfiles
      }

    } catch (error) {
      if (error.code === 'ENOENT') {
        // Return empty profiles structure if file doesn't exist
        return {
          version: '1.0.0',
          profiles: {}
        }
      }
      throw new Error(`Failed to list profiles: ${error.message}`)
    }
  }

  /**
   * Get a specific profile
   * @param {string} name - Profile name to get
   * @returns {Promise<Object>} Profile data
   */
  static async getProfile(name) {
    try {
      const profilesData = await this.listProfiles()
      
      if (!profilesData.profiles[name]) {
        throw new Error(`Profile '${name}' not found`)
      }

      return profilesData.profiles[name]
    } catch (error) {
      throw new Error(`Failed to get profile: ${error.message}`)
    }
  }

  /**
   * Update a profile
   * @param {string} name - Profile name to update
   * @param {Object} updates - Updates to apply
   * @returns {Promise<void>}
   */
  static async updateProfile(name, updates) {
    try {
      // Check if identity exists
      if (!this.identityExists()) {
        throw new Error('Identity not found. Run "hyperident init" to create one.')
      }

      // Load existing profiles
      const profilesData = await FileManager.readProfiles()

      // Check if profile exists
      if (!profilesData.profiles[name]) {
        throw new Error(`Profile '${name}' not found`)
      }

      // Apply updates
      const profile = profilesData.profiles[name]
      if (updates.description !== undefined) {
        profile.description = updates.description
      }
      profile.lastModified = new Date().toISOString()

      // Save updated profiles
      await FileManager.writeProfiles(profilesData)

    } catch (error) {
      throw new Error(`Failed to update profile: ${error.message}`)
    }
  }

  /**
   * Check if a profile exists
   * @param {string} name - Profile name to check
   * @returns {Promise<boolean>} True if profile exists
   */
  static async profileExists(name) {
    try {
      const profilesData = await this.listProfiles()
      return !!profilesData.profiles[name]
    } catch (error) {
      return false
    }
  }

  /**
   * Validate profile name format
   * @param {string} name - Profile name to validate
   * @returns {boolean} True if valid
   */
  static validateProfileName(name) {
    // Check basic requirements
    if (!name || typeof name !== 'string') {
      return false
    }

    // Check length (1-32 characters)
    if (name.length < 1 || name.length > 32) {
      return false
    }

    // Check for reserved names
    const reservedNames = ['default', 'config', 'temp', 'system', 'admin']
    if (reservedNames.includes(name.toLowerCase())) {
      return false
    }

    // Check format: alphanumeric and hyphens only
    const validFormat = /^[a-zA-Z0-9-]+$/
    if (!validFormat.test(name)) {
      return false
    }

    // Cannot start or end with hyphen
    if (name.startsWith('-') || name.endsWith('-')) {
      return false
    }

    // Cannot have consecutive hyphens
    if (name.includes('--')) {
      return false
    }

    return true
  }

  /**
   * Derive hyperswarm topic from profile name
   * @param {string} profileName - Profile name
   * @param {string|null} passphrase - Optional passphrase for encrypted identities
   * @returns {Promise<Buffer>} 32-byte topic for hyperswarm
   */
  static async deriveTopicFromProfile(profileName, passphrase = null) {
    try {
      // Validate inputs
      if (!profileName || typeof profileName !== 'string') {
        throw new Error('Profile name is required and must be a string')
      }

      // Check if identity exists
      if (!this.identityExists()) {
        throw new Error('Identity not found. Run "hyperident init" to create one.')
      }

      // Check if profile exists
      const profileExists = await this.profileExists(profileName)
      if (!profileExists) {
        throw new Error(`Profile '${profileName}' not found. Create it first with "hyperident create-profile ${profileName}".`)
      }

      // Load identity (handle encrypted identities)
      const identity = await this.loadIdentity(passphrase)

      // Create IdentityKey instance from mnemonic
      const identityKey = await IdentityKey.from({ mnemonic: identity.mnemonic })

      // Hash profile name with SHA256
      const profileHash = CryptoManager.hash(Buffer.from(profileName, 'utf8'))

      // Use keet-identity-key to derive encryption key from profile hash
      const encryptionKey = identityKey.getEncryptionKey(profileHash)

      // Return first 32 bytes as hyperswarm topic
      return encryptionKey.subarray(0, 32)

    } catch (error) {
      throw new Error(`Failed to derive topic from profile: ${error.message}`)
    }
  }

  /**
   * Get hyperswarm address string for a profile
   * @param {string} profileName - Profile name
   * @param {string|null} passphrase - Optional passphrase for encrypted identities
   * @returns {Promise<string>} Hyperswarm address as hex string
   */
  static async getProfileAddress(profileName, passphrase = null) {
    try {
      const topic = await this.deriveTopicFromProfile(profileName, passphrase)
      return topic.toString('hex')
    } catch (error) {
      throw new Error(`Failed to get profile address: ${error.message}`)
    }
  }

  /**
   * Get cached profile address (fast path)
   * @param {string} profileName - Profile name
   * @param {string|null} passphrase - Optional passphrase for encrypted identities
   * @returns {Promise<string>} Cached address as hex string
   */
  static async getCachedProfileAddress(profileName, passphrase = null) {
    try {
      const profilesData = await this.listProfiles()
      const profile = profilesData.profiles[profileName]
      
      if (!profile) {
        throw new Error(`Profile '${profileName}' not found`)
      }
      
      // Return cached address if available, otherwise generate and cache
      if (profile.address) {
        return profile.address
      } else {
        // Address not cached, generate and cache it
        const address = await this.getProfileAddress(profileName, passphrase)
        await this.updateProfileAddress(profileName, address)
        return address
      }
    } catch (error) {
      throw new Error(`Failed to get cached profile address: ${error.message}`)
    }
  }

  /**
   * Update cached address for a profile
   * @param {string} profileName - Profile name
   * @param {string} address - Address as hex string
   * @returns {Promise<void>}
   */
  static async updateProfileAddress(profileName, address) {
    try {
      const profilesData = await FileManager.readProfiles()
      
      if (!profilesData.profiles[profileName]) {
        throw new Error(`Profile '${profileName}' not found`)
      }
      
      profilesData.profiles[profileName].address = address
      profilesData.profiles[profileName].lastModified = new Date().toISOString()
      
      await FileManager.writeProfiles(profilesData)
    } catch (error) {
      throw new Error(`Failed to update profile address: ${error.message}`)
    }
  }

  /**
   * Refresh all cached addresses
   * @param {string|null} passphrase - Optional passphrase for encrypted identities
   * @returns {Promise<void>}
   */
  static async refreshAllAddresses(passphrase = null) {
    try {
      const profilesData = await this.listProfiles()
      const profileNames = Object.keys(profilesData.profiles)
      
      for (const profileName of profileNames) {
        const address = await this.getProfileAddress(profileName, passphrase)
        await this.updateProfileAddress(profileName, address)
      }
    } catch (error) {
      throw new Error(`Failed to refresh all addresses: ${error.message}`)
    }
  }

  /**
   * Export addresses for external programs
   * @returns {Promise<Object>} Addresses data
   */
  static async exportAddresses() {
    try {
      const profilesData = await this.listProfiles()
      const addresses = {}
      
      for (const [name, profile] of Object.entries(profilesData.profiles)) {
        addresses[name] = {
          address: profile.address || null,
          description: profile.description,
          created: profile.created,
          lastModified: profile.lastModified
        }
      }
      
      return {
        version: '1.0.0',
        exported: new Date().toISOString(),
        addresses
      }
    } catch (error) {
      throw new Error(`Failed to export addresses: ${error.message}`)
    }
  }

  /**
   * Export addresses as JSON string
   * @returns {Promise<string>} JSON string of addresses
   */
  static async exportAddressesJSON() {
    try {
      const data = await this.exportAddresses()
      return JSON.stringify(data, null, 2)
    } catch (error) {
      throw new Error(`Failed to export addresses as JSON: ${error.message}`)
    }
  }

  /**
   * Export enhanced addresses with multiple formats for application integration
   * @param {string|null} passphrase - Optional passphrase for encrypted identities
   * @returns {Promise<Object>} Enhanced addresses data with multiple formats
   */
  static async exportAddressesEnhanced(passphrase = null) {
    try {
      const profilesData = await this.listProfiles()
      const identity = await this.loadIdentity(passphrase)
      
      const addresses = {}
      const envVars = {}
      const hyperswarmTopics = {}
      
      for (const [name, profile] of Object.entries(profilesData.profiles)) {
        // Get or generate address
        let address = profile.address
        if (!address) {
          address = await this.getProfileAddress(name, passphrase)
          await this.updateProfileAddress(name, address)
        }
        
        // Generate topic buffer for hyperswarm
        const topic = await this.deriveTopicFromProfile(name, passphrase)
        
        addresses[name] = {
          address: address,
          topic: topic.toString('hex'),
          description: profile.description,
          created: profile.created,
          lastModified: profile.lastModified,
          formats: {
            hex: address,
            buffer: `Buffer.from('${address}', 'hex')`,
            uint8Array: `new Uint8Array(Buffer.from('${address}', 'hex'))`,
            base64: Buffer.from(address, 'hex').toString('base64')
          }
        }
        
        // Environment variable format
        const envName = name.toUpperCase().replace(/[^A-Z0-9]/g, '_')
        envVars[`HYPERIDENT_${envName}_ADDRESS`] = address
        envVars[`HYPERIDENT_${envName}_TOPIC`] = topic.toString('hex')
        
        // Hyperswarm topic format
        hyperswarmTopics[name] = {
          topic: topic.toString('hex'),
          buffer: Array.from(topic),
          usage: `hyperswarm.join(Buffer.from('${topic.toString('hex')}', 'hex'))`
        }
      }
      
      return {
        version: '1.1.0',
        exported: new Date().toISOString(),
        identity: {
          publicKey: identity.publicKey,
          algorithm: identity.algorithm,
          encrypted: identity.encrypted
        },
        addresses,
        formats: {
          envVars,
          hyperswarmTopics
        },
        usage: {
          javascript: {
            import: "import { IdentityManager } from 'hyperident'",
            getAddress: "const address = await IdentityManager.getProfileAddress('profileName')",
            getTopic: "const topic = await IdentityManager.deriveTopicFromProfile('profileName')"
          },
          hyperswarm: {
            join: "swarm.join(Buffer.from(address, 'hex'))",
            listen: "swarm.join(Buffer.from(address, 'hex'), { server: true, client: false })",
            connect: "swarm.join(Buffer.from(address, 'hex'), { server: false, client: true })"
          },
          environment: {
            description: "Set these environment variables in your application",
            example: "export HYPERIDENT_WORK_ADDRESS=abc123..."
          }
        }
      }
    } catch (error) {
      throw new Error(`Failed to export enhanced addresses: ${error.message}`)
    }
  }

  /**
   * Export enhanced addresses as JSON string
   * @param {string|null} passphrase - Optional passphrase for encrypted identities
   * @returns {Promise<string>} JSON string of enhanced addresses
   */
  static async exportAddressesEnhancedJSON(passphrase = null) {
    try {
      const data = await this.exportAddressesEnhanced(passphrase)
      return JSON.stringify(data, null, 2)
    } catch (error) {
      throw new Error(`Failed to export enhanced addresses as JSON: ${error.message}`)
    }
  }

  /**
   * Validate profile exists before operations
   * @param {string} profileName - Profile name to validate
   * @returns {Promise<void>}
   */
  static async validateProfile(profileName) {
    if (!profileName || typeof profileName !== 'string') {
      throw new Error('Profile name is required and must be a string')
    }

    if (!this.identityExists()) {
      throw new Error('Identity not found. Run "hyperident init" to create one.')
    }

    const exists = await this.profileExists(profileName)
    if (!exists) {
      throw new Error(`Profile '${profileName}' not found. Create it first with "hyperident create-profile ${profileName}".`)
    }
  }

  // Data Signing Methods (following schema-sheets-cli patterns)

  /**
   * Sign arbitrary data using device identity
   * @param {Buffer} data - Data to sign
   * @param {string|null} passphrase - Optional passphrase for encrypted identities
   * @returns {Promise<Buffer>} Signature proof
   */
  static async signData(data, passphrase = null) {
    try {
      if (!Buffer.isBuffer(data)) {
        throw new Error('Data must be a Buffer')
      }

      // Load device chain for signing
      const deviceChain = await this.loadDeviceChain()
      
      // Load bootstrap proof for signing
      const bootstrapProof = await FileManager.readBootstrapProof()
      
      // Verify device chain is valid
      const verification = await this.verifyDeviceProof(deviceChain.proof, passphrase)
      if (!verification) {
        throw new Error('Device chain is not valid for signing')
      }

      // Sign data using keet-identity-key attestData pattern
      const proof = IdentityKey.attestData(
        data,
        deviceChain.keyPair,
        bootstrapProof
      )

      return proof
    } catch (error) {
      throw new Error(`Failed to sign data: ${error.message}`)
    }
  }

  /**
   * Verify a signature against data
   * @param {Buffer} proof - Signature proof to verify
   * @param {Buffer} data - Original data that was signed
   * @returns {Promise<Object|null>} Verification result or null if invalid
   */
  static async verifySignature(proof, data) {
    try {
      if (!Buffer.isBuffer(proof)) {
        throw new Error('Proof must be a Buffer')
      }
      if (!Buffer.isBuffer(data)) {
        throw new Error('Data must be a Buffer')
      }

      // Verify signature using keet-identity-key
      const verification = IdentityKey.verify(proof, data)
      
      return verification
    } catch (error) {
      throw new Error(`Failed to verify signature: ${error.message}`)
    }
  }

  /**
   * Export signing configuration for applications (following schema-sheets-cli pattern)
   * @param {string|null} passphrase - Optional passphrase for encrypted identities
   * @returns {Promise<Object>} Signing configuration
   */
  static async exportSigningConfig(passphrase = null) {
    try {
      // Load identity
      const identity = await this.loadIdentity(passphrase)
      const identityKey = await IdentityKey.from({ mnemonic: identity.mnemonic })
      
      // Load device chain
      const deviceChain = await this.loadDeviceChain()
      
      // Load bootstrap proof
      const bootstrapProof = await FileManager.readBootstrapProof()
      
      // Verify device chain is valid
      const verification = await this.verifyDeviceProof(deviceChain.proof, passphrase)
      if (!verification) {
        throw new Error('Device chain is not valid for signing configuration export')
      }

      // Return signing configuration in schema-sheets-cli format
      return {
        identityPublicKey: identityKey.identityKeyPair.publicKey.toString('hex'),
        devicePublicKey: deviceChain.keyPair.publicKey.toString('hex'),
        deviceSecretKey: deviceChain.keyPair.secretKey.toString('hex'),
        bootstrapProof: bootstrapProof.toString('hex')
      }
    } catch (error) {
      throw new Error(`Failed to export signing config: ${error.message}`)
    }
  }

  // Device Chain Management Methods

  /**
   * Save device chain (keys, proof, metadata)
   * @param {Object} deviceKeyPair - Device keypair with publicKey and secretKey
   * @param {Buffer} deviceProof - Device attestation proof
   * @param {string} deviceName - Name for this device
   * @returns {Promise<void>}
   */
  static async saveDeviceChain(deviceKeyPair, deviceProof, deviceName) {
    try {
      // Save device keys
      await FileManager.writeDevicePrivateKey(deviceKeyPair.secretKey)
      await FileManager.writeDevicePublicKey(deviceKeyPair.publicKey)
      
      // Save device proof (for device chain verification)
      await FileManager.writeDeviceProof(deviceProof)
      
      // Save bootstrap proof (for data signing operations)
      // This is the same as device proof for root devices, but enables signing
      await FileManager.writeBootstrapProof(deviceProof)
      
      // Save device metadata
      const deviceMetadata = {
        name: deviceName,
        created: new Date().toISOString(),
        publicKey: deviceKeyPair.publicKey.toString('hex'),
        version: '1.0.0'
      }
      await FileManager.writeDeviceMetadata(deviceMetadata)
      
    } catch (error) {
      throw new Error(`Failed to save device chain: ${error.message}`)
    }
  }

  /**
   * Load device chain data
   * @returns {Promise<Object>} Device chain data
   */
  static async loadDeviceChain() {
    try {
      const secretKey = await FileManager.readDevicePrivateKey()
      const publicKey = await FileManager.readDevicePublicKey()
      const proof = await FileManager.readDeviceProof()
      const metadata = await FileManager.readDeviceMetadata()
      
      return {
        keyPair: { secretKey, publicKey },
        proof,
        metadata
      }
    } catch (error) {
      throw new Error(`Failed to load device chain: ${error.message}`)
    }
  }

  /**
   * Verify device proof chain
   * @param {Buffer} proof - Device proof to verify
   * @param {string|null} passphrase - Optional passphrase for encrypted identities
   * @returns {Promise<Object|null>} Verification result or null if invalid
   */
  static async verifyDeviceProof(proof, passphrase = null) {
    try {
      // Load identity to get expected root key
      const identity = await this.loadIdentity(passphrase)
      const identityKey = await IdentityKey.from({ mnemonic: identity.mnemonic })
      const expectedRootKey = identityKey.identityKeyPair.publicKey
      
      // Verify the proof using keet-identity-key
      const verification = IdentityKey.verify(proof)
      
      if (!verification) {
        return null // Invalid proof
      }
      
      // Check if proof links back to our root identity
      if (!verification.identityPublicKey.equals(expectedRootKey)) {
        return null // Proof is valid but for different identity
      }
      
      return verification
    } catch (error) {
      throw new Error(`Failed to verify device proof: ${error.message}`)
    }
  }

  /**
   * Check if device chain exists and is valid
   * @param {string|null} passphrase - Optional passphrase for encrypted identities
   * @returns {Promise<boolean>} True if device chain exists and is valid
   */
  static async deviceChainExists(passphrase = null) {
    try {
      // Check if all device files exist
      const filesExist = await FileManager.deviceChainExists()
      if (!filesExist) {
        return false
      }
      
      // Load and verify device proof
      const deviceChain = await this.loadDeviceChain()
      const verification = await this.verifyDeviceProof(deviceChain.proof, passphrase)
      
      return verification !== null
    } catch (error) {
      return false
    }
  }

  /**
   * Attest a new device using existing device
   * @param {Buffer} newDevicePublicKey - Public key of new device to attest
   * @param {string|null} passphrase - Optional passphrase for encrypted identities
   * @returns {Promise<Buffer>} New device proof
   */
  static async attestNewDevice(newDevicePublicKey, passphrase = null) {
    try {
      // Load current device chain
      const currentDevice = await this.loadDeviceChain()
      
      // Verify current device is valid
      const verification = await this.verifyDeviceProof(currentDevice.proof, passphrase)
      if (!verification) {
        throw new Error('Current device is not properly attested')
      }
      
      // Create attestation for new device
      const newProof = IdentityKey.attestDevice(
        newDevicePublicKey,
        currentDevice.keyPair,
        currentDevice.proof
      )
      
      return newProof
    } catch (error) {
      throw new Error(`Failed to attest new device: ${error.message}`)
    }
  }

  /**
   * Export device bundle for cross-device sync
   * @param {string|null} passphrase - Optional passphrase for encrypted identities
   * @returns {Promise<Object>} Device bundle for import on new device
   */
  static async exportDeviceBundle(passphrase = null) {
    try {
      // Load identity and device chain
      const identity = await this.loadIdentity(passphrase)
      const deviceChain = await this.loadDeviceChain()
      const profiles = await this.listProfiles()
      
      // Verify device chain is valid
      const verification = await this.verifyDeviceProof(deviceChain.proof, passphrase)
      if (!verification) {
        throw new Error('Device chain is not valid')
      }
      
      return {
        version: '1.0.0',
        exported: new Date().toISOString(),
        identity: {
          mnemonic: identity.mnemonic,
          publicKey: identity.publicKey,
          created: identity.created,
          algorithm: identity.algorithm
        },
        deviceChain: {
          keyPair: {
            publicKey: deviceChain.keyPair.publicKey.toString('hex'),
            secretKey: deviceChain.keyPair.secretKey.toString('hex')
          },
          proof: deviceChain.proof.toString('hex'),
          metadata: deviceChain.metadata
        },
        profiles: profiles.profiles
      }
    } catch (error) {
      throw new Error(`Failed to export device bundle: ${error.message}`)
    }
  }

  /**
   * Import device bundle and create new device attestation
   * @param {Object} bundle - Device bundle from export
   * @param {string} newDeviceName - Name for the new device
   * @param {string|null} passphrase - Optional passphrase for encryption
   * @returns {Promise<void>}
   */
  static async importDeviceBundle(bundle, newDeviceName, passphrase = null) {
    try {
      // Validate bundle format
      if (!bundle || !bundle.identity || !bundle.deviceChain) {
        throw new Error('Invalid device bundle format')
      }
      
      // Generate new device keypair
      const newDeviceKeyPair = await CryptoManager.generateRawDeviceKeyPair()
      
      // Reconstruct parent device data
      const parentKeyPair = {
        publicKey: Buffer.from(bundle.deviceChain.keyPair.publicKey, 'hex'),
        secretKey: Buffer.from(bundle.deviceChain.keyPair.secretKey, 'hex')
      }
      const parentProof = Buffer.from(bundle.deviceChain.proof, 'hex')
      
      // Create attestation for new device
      const newDeviceProof = IdentityKey.attestDevice(
        newDeviceKeyPair.publicKey,
        parentKeyPair,
        parentProof
      )
      
      // Save identity
      await this.saveIdentity(bundle.identity.mnemonic, passphrase)
      
      // Save new device chain
      await this.saveDeviceChain(newDeviceKeyPair, newDeviceProof, newDeviceName)
      
      // Import profiles
      if (bundle.profiles && Object.keys(bundle.profiles).length > 0) {
        const profilesData = {
          version: '1.0.0',
          profiles: bundle.profiles
        }
        await FileManager.writeProfiles(profilesData)
      }
      
    } catch (error) {
      throw new Error(`Failed to import device bundle: ${error.message}`)
    }
  }
}