/** * Identity Manager Implementation * * Manages hierarchical user/device identity system with HD key derivation */ import { LoggerComponent } from 'ior:gitea:gitea.metatrom.net:universal-components/logger@1.0.0'; import { ed25519 } from '@noble/curves/ed25519'; import { sha256 } from '@noble/hashes/sha256'; import { HDKey } from '@scure/bip32'; import * as bip39 from 'bip39'; import type { DeviceIdentity, IdentityConfig, IIdentityManager, IIdentityStorage, UserIdentity, } from '../interfaces/IIdentity'; export class IdentityManager implements IIdentityManager { private masterKey?: HDKey; private userIdentity?: UserIdentity; private devices: Map = new Map(); private currentDeviceIndex: number = 0; private storage: IIdentityStorage; private config: Required; private initialized: boolean = false; private logger = new LoggerComponent('IdentityManager'); constructor(storage: IIdentityStorage, config?: IdentityConfig) { this.storage = storage; this.config = { storagePrefix: config?.storagePrefix || '@IdentityManager', mnemonicStrength: config?.mnemonicStrength || 256, deviceNameProvider: config?.deviceNameProvider || this.defaultDeviceNameProvider, }; } async initialize(): Promise { if (this.initialized && this.userIdentity) { return this.userIdentity; } // Try to load existing identity const existingIdentity = await this.loadIdentity(); if (existingIdentity) { this.initialized = true; return existingIdentity; } // Create new identity const newIdentity = await this.createNewIdentity(); this.initialized = true; return newIdentity; } async createNewIdentity(): Promise { // Generate new mnemonic const mnemonic = bip39.generateMnemonic(this.config.mnemonicStrength); const seed = await bip39.mnemonicToSeed(mnemonic); // Create master HD key this.masterKey = HDKey.fromMasterSeed(seed); // Get master public key const masterPublicKey = this.masterKey.publicKey!; // Create user ID from hash of public key const userId = this.hashPublicKey(masterPublicKey); this.userIdentity = { userId, publicKey: masterPublicKey, mnemonic, }; // Save to storage await this.saveIdentity(); return this.userIdentity; } async broadcastPeerIdUpdate( oldPeerId: string, newPeerId: string, sendProtocolData?: (peerId: string, protocolId: string, data: Uint8Array) => Promise, ): Promise { if (!sendProtocolData) { this.logger.warn( '[IdentityManager] No sendProtocolData function provided, cannot broadcast peer ID update', ); return; } const userIdentity = await this.getUserIdentity(); const currentDevice = await this.getCurrentDevice(); // Create update notification const updateData = { userId: userIdentity.userId, oldPeerId, newPeerId, deviceName: currentDevice.deviceName, timestamp: Date.now(), }; this.logger.info('[IdentityManager] Broadcasting peer ID update:', updateData); // Encode the update data const encoded = encodeURIComponent(JSON.stringify(updateData)); const messageBytes = []; for (let i = 0; i < encoded.length; i++) { if (encoded[i] === '%') { const hex = encoded.substring(i + 1, i + 3); messageBytes.push(Number.parseInt(hex, 16)); i += 2; } else { messageBytes.push(encoded.charCodeAt(i)); } } const data = new Uint8Array(messageBytes); // Get all other devices for this user const allDevices = await this.getRegisteredDevices(); const otherDevices = allDevices.filter((d) => d.deviceId !== newPeerId); // Send update to each other device for (const device of otherDevices) { try { await sendProtocolData(device.deviceId, '/metatrom/identity/peer-update/1.0.0', data); this.logger.debug( `[IdentityManager] Sent peer ID update to ${device.deviceName} (${device.deviceId})`, ); } catch (err) { this.logger.warn(`[IdentityManager] Failed to send update to ${device.deviceId}:`, err); } } } async restoreFromMnemonic(mnemonic: string): Promise { this.logger.debug('[IdentityManager] Validating mnemonic...'); this.logger.debug('[IdentityManager] Mnemonic length:', mnemonic.length); this.logger.debug('[IdentityManager] Mnemonic words:', mnemonic.trim().split(/\s+/).length); const words = mnemonic.trim().split(/\s+/); this.logger.debug('[IdentityManager] First word:', words[0]); this.logger.debug('[IdentityManager] Last word:', words[words.length - 1]); // Check each word individually const wordlist = bip39.wordlists.english; const invalidWords = words.filter((word) => !wordlist.includes(word)); if (invalidWords.length > 0) { this.logger.error('[IdentityManager] Invalid words found:', JSON.stringify(invalidWords)); this.logger.error('[IdentityManager] These words are not in the BIP39 English wordlist'); // Show what each invalid word looks like in detail for (let index = 0; index < invalidWords.length; index++) { const word = invalidWords[index]; this.logger.error( `[IdentityManager] Invalid word ${index + 1}: "${word}" (length: ${word.length})`, ); // Check if it's a truncated version of a valid word const possibleMatches = wordlist.filter((w) => w.startsWith(word)); if (possibleMatches.length > 0) { this.logger.error( `[IdentityManager] Possible matches: ${possibleMatches.slice(0, 5).join(', ')}`, ); } } } if (!bip39.validateMnemonic(mnemonic)) { this.logger.error('[IdentityManager] BIP39 validation failed for mnemonic'); this.logger.error('[IdentityManager] This could be due to invalid words or checksum failure'); throw new Error('Invalid mnemonic phrase'); } // Keep track of current device before restoration const currentDeviceIdKey = this.getStorageKey('currentDeviceId'); const currentDeviceId = await this.storage.getItem(currentDeviceIdKey); let currentDeviceBackup: DeviceIdentity | undefined; if (currentDeviceId && this.devices.has(currentDeviceId)) { // Backup current device info currentDeviceBackup = { ...this.devices.get(currentDeviceId)! }; this.logger.debug( '[IdentityManager] Backing up current device:', currentDeviceBackup.deviceName, ); } const seed = await bip39.mnemonicToSeed(mnemonic); this.masterKey = HDKey.fromMasterSeed(seed); const masterPublicKey = this.masterKey.publicKey!; const userId = this.hashPublicKey(masterPublicKey); this.userIdentity = { userId, publicKey: masterPublicKey, mnemonic, }; // Clear existing devices as they belong to the old identity this.devices.clear(); // Find the next available device index by checking existing peer IDs // This ensures each device gets a unique derivation path this.currentDeviceIndex = await this.findNextAvailableDeviceIndex(); this.logger.debug('[IdentityManager] Using device index:', this.currentDeviceIndex); // If we had a current device, recreate it with the new identity if (currentDeviceBackup) { this.logger.debug('[IdentityManager] Recreating current device with new identity'); // Create a new device with the same name but derived from the new master key const newDevice = await this.createDeviceIdentity(currentDeviceBackup.deviceName); // Update the current device ID reference await this.storage.setItem(currentDeviceIdKey, newDevice.deviceId); this.logger.debug('[IdentityManager] New device created with ID:', newDevice.deviceId); this.logger.debug( '[IdentityManager] Device uses derivation path index:', this.currentDeviceIndex - 1, ); } await this.saveIdentity(); this.initialized = true; return this.userIdentity; } async createDeviceIdentity(deviceName: string): Promise { if (!this.masterKey) { throw new Error('Master identity not initialized'); } // Use BIP44-like path: m/44'/0'/0'/0/index const derivationPath = `m/44'/0'/0'/0/${this.currentDeviceIndex}`; // Derive child key const childKey = this.masterKey.derive(derivationPath); // Convert to Ed25519 keypair for libp2p const seed = childKey.privateKey!.slice(0, 32); this.logger.debug('[IdentityManager] === DEVICE IDENTITY CREATION ==='); this.logger.debug('[IdentityManager] Device index:', this.currentDeviceIndex); this.logger.debug('[IdentityManager] Derivation path:', derivationPath); this.logger.debug('[IdentityManager] Seed length:', seed.length); this.logger.debug( '[IdentityManager] Seed (hex):', Array.from(seed) .map((b) => b.toString(16).padStart(2, '0')) .join(''), ); // Using @noble/curves/ed25519 getPublicKey const keyPair = ed25519.getPublicKey(seed); this.logger.debug('[IdentityManager] Public key from ed25519.getPublicKey:', keyPair); this.logger.debug( '[IdentityManager] Public key type:', typeof keyPair, keyPair.constructor.name, ); this.logger.debug('[IdentityManager] Public key length:', keyPair.length); this.logger.debug( '[IdentityManager] Public key (hex):', Array.from(keyPair) .map((b) => b.toString(16).padStart(2, '0')) .join(''), ); // Create libp2p compatible peer ID const deviceId = await this.createPeerIdFromEd25519(seed, keyPair); const device: DeviceIdentity = { deviceId, deviceName, publicKey: keyPair, privateKey: seed, derivationPath, createdAt: Date.now(), }; // Store device this.devices.set(deviceId, device); this.currentDeviceIndex++; // Update the highest device index in storage const deviceIndexKey = this.getStorageKey('highestDeviceIndex'); await this.storage.setItem(deviceIndexKey, this.currentDeviceIndex.toString()); await this.saveDevices(); return device; } /** * Calculate what the peer ID will be for a given device index * This is useful during pairing to know what peer ID the new device will have */ async calculatePeerIdForIndex(deviceIndex: number): Promise { if (!this.masterKey) { throw new Error('Master identity not initialized'); } // Use BIP44-like path: m/44'/0'/0'/0/index const derivationPath = `m/44'/0'/0'/0/${deviceIndex}`; // Derive child key const childKey = this.masterKey.derive(derivationPath); // Convert to Ed25519 keypair for libp2p const seed = childKey.privateKey!.slice(0, 32); const keyPair = ed25519.getPublicKey(seed); // Create libp2p compatible peer ID return await this.createPeerIdFromEd25519(seed, keyPair); } async getCurrentDevice(): Promise { // Get a stable device ID from storage first const deviceIdKey = this.getStorageKey('currentDeviceId'); const storedDeviceId = await this.storage.getItem(deviceIdKey); // If we have a stored device ID, try to find that device if (storedDeviceId) { const existingDevice = this.devices.get(storedDeviceId); if (existingDevice) { // Update last seen existingDevice.lastSeen = Date.now(); await this.saveDevices(); return existingDevice; } else { // Try to find a device that matches the current platform const deviceName = await this.config.deviceNameProvider(); const matchingDevice = Array.from(this.devices.values()).find( (d) => d.deviceName === deviceName || d.deviceName.startsWith(deviceName.split('-')[0]), ); if (matchingDevice) { // Update the stored device ID to this device await this.storage.setItem(deviceIdKey, matchingDevice.deviceId); matchingDevice.lastSeen = Date.now(); await this.saveDevices(); return matchingDevice; } } } // Get device name from platform const deviceName = await this.config.deviceNameProvider(); // Check if we already have a device with this exact name (fallback) const existingDevice = Array.from(this.devices.values()).find( (d) => d.deviceName === deviceName, ); if (existingDevice) { // Store this as the current device await this.storage.setItem(deviceIdKey, existingDevice.deviceId); existingDevice.lastSeen = Date.now(); await this.saveDevices(); return existingDevice; } // Create new device identity const newDevice = await this.createDeviceIdentity(deviceName); // Store as current device await this.storage.setItem(deviceIdKey, newDevice.deviceId); return newDevice; } async getRegisteredDevices(): Promise { return Array.from(this.devices.values()); } async removeDevice(deviceId: string): Promise { // Check if this is the current device const currentDeviceIdKey = this.getStorageKey('currentDeviceId'); const currentDeviceId = await this.storage.getItem(currentDeviceIdKey); if (currentDeviceId === deviceId) { throw new Error('Cannot remove the current device'); } this.devices.delete(deviceId); await this.saveDevices(); } async cleanupDuplicateDevices(): Promise { // Get current device ID to avoid removing it const currentDeviceIdKey = this.getStorageKey('currentDeviceId'); const currentDeviceId = await this.storage.getItem(currentDeviceIdKey); // Group devices by name const devicesByName = new Map(); for (const device of this.devices.values()) { const baseName = device.deviceName.split('-')[0]; // Get base name without random suffix if (!devicesByName.has(baseName)) { devicesByName.set(baseName, []); } devicesByName.get(baseName)!.push(device); } const devicesToRemove: string[] = []; // For each group of devices with the same base name for (const [_baseName, devices] of devicesByName) { if (devices.length > 1) { // Sort by lastSeen (most recent first) and createdAt as fallback devices.sort((a, b) => { const aTime = a.lastSeen || a.createdAt; const bTime = b.lastSeen || b.createdAt; return bTime - aTime; }); // Keep the most recent one (or the current device if it's in this group) let deviceToKeep = devices[0]; for (const device of devices) { if (device.deviceId === currentDeviceId) { deviceToKeep = device; break; } } // Mark others for removal for (const device of devices) { if (device.deviceId !== deviceToKeep.deviceId) { devicesToRemove.push(device.deviceId); } } } } // Remove duplicate devices for (const deviceId of devicesToRemove) { this.devices.delete(deviceId); } if (devicesToRemove.length > 0) { await this.saveDevices(); } return devicesToRemove.length; } async cleanupInactiveDevices(daysInactive: number = 30): Promise { const now = Date.now(); const cutoffTime = now - daysInactive * 24 * 60 * 60 * 1000; this.logger.info(`[IdentityManager] Cleaning up devices inactive for ${daysInactive} days`); this.logger.debug(`[IdentityManager] Current time: ${new Date(now).toISOString()}`); this.logger.debug(`[IdentityManager] Cutoff time: ${new Date(cutoffTime).toISOString()}`); // Get current device ID to avoid removing it const currentDeviceIdKey = this.getStorageKey('currentDeviceId'); const currentDeviceId = await this.storage.getItem(currentDeviceIdKey); this.logger.debug(`[IdentityManager] Current device ID: ${currentDeviceId}`); const devicesToRemove: string[] = []; for (const [deviceId, device] of this.devices) { // Never remove current device if (deviceId === currentDeviceId) { this.logger.debug(`[IdentityManager] Skipping current device: ${deviceId}`); continue; } // Check last seen time const lastSeen = device.lastSeen || device.createdAt; this.logger.debug( `[IdentityManager] Device ${device.deviceName} (${deviceId}) last seen: ${new Date(lastSeen).toISOString()}`, ); if (lastSeen < cutoffTime) { this.logger.debug(`[IdentityManager] Marking for removal: ${device.deviceName}`); devicesToRemove.push(deviceId); } } // Remove inactive devices for (const deviceId of devicesToRemove) { this.devices.delete(deviceId); } if (devicesToRemove.length > 0) { await this.saveDevices(); this.logger.info(`[IdentityManager] Removed ${devicesToRemove.length} inactive devices`); } else { this.logger.info(`[IdentityManager] No inactive devices to remove`); } return devicesToRemove.length; } async getLibp2pKeypair(): Promise<{ privateKey: Uint8Array; publicKey: Uint8Array }> { const device = await this.getCurrentDevice(); // Marshal the Ed25519 private key for libp2p compatibility // The protobuf format for Ed25519 keys is: // message PrivateKey { // Type key_type = 1; // enum where Ed25519 = 1 // bytes data = 2; // the actual key data // } // // In protobuf wire format: // - Field 1 (key_type), varint: 0x08 (field number 1, wire type 0) followed by 0x01 (Ed25519 enum value) // - Field 2 (data), length-delimited: 0x12 (field number 2, wire type 2) followed by length and data // For Ed25519, the data field contains 64 bytes: 32-byte private key + 32-byte public key const keyData = new Uint8Array(64); keyData.set(device.privateKey, 0); keyData.set(device.publicKey, 32); // Build the protobuf message const protobufMessage = new Uint8Array([ 0x08, 0x01, // Field 1: key_type = Ed25519 (1) 0x12, 0x40, // Field 2: data field with length 0x40 (64 bytes) ...keyData, // The 64 bytes of key data ]); this.logger.debug('[IdentityManager] Marshaled private key for libp2p:'); this.logger.debug('[IdentityManager] - Total length:', protobufMessage.length); this.logger.debug( '[IdentityManager] - Header:', Array.from(protobufMessage.slice(0, 4)) .map((b) => `0x${b.toString(16).padStart(2, '0')}`) .join(' '), ); this.logger.debug( '[IdentityManager] - Private key part (first 8 bytes):', Array.from(protobufMessage.slice(4, 12)) .map((b) => b.toString(16).padStart(2, '0')) .join(''), ); this.logger.debug( '[IdentityManager] - Public key part (first 8 bytes):', Array.from(protobufMessage.slice(36, 44)) .map((b) => b.toString(16).padStart(2, '0')) .join(''), ); return { privateKey: protobufMessage, publicKey: device.publicKey, }; } async updateDevicePeerId(peerId: string): Promise { const deviceIdKey = this.getStorageKey('currentDeviceId'); const storedDeviceId = await this.storage.getItem(deviceIdKey); if (!storedDeviceId) { this.logger.error('[IdentityManager] No current device ID found in storage'); return; } // First check if the device exists with the stored ID let device = this.devices.get(storedDeviceId); // If not found with stored ID, check if it's already been updated to the new peer ID if (!device) { device = this.devices.get(peerId); if (device) { // Update storage to reflect the current state await this.storage.setItem(deviceIdKey, peerId); return; } } if (!device) { // As a fallback, find the device by name pattern const deviceName = await this.config.deviceNameProvider(); const matchingDevice = Array.from(this.devices.values()).find( (d) => d.deviceName === deviceName || d.deviceName.startsWith(deviceName.split('-')[0]), ); if (matchingDevice) { device = matchingDevice; } else { return; } } // Update the device's peer ID to match what libp2p generated if (device.deviceId !== peerId) { // Remove old entry this.devices.delete(device.deviceId); // Update device ID device.deviceId = peerId; // Add with new ID this.devices.set(peerId, device); // Update storage await this.saveDevices(); await this.storage.setItem(deviceIdKey, peerId); } } getMnemonic(): string | undefined { return this.userIdentity?.mnemonic; } getUserIdentity(): UserIdentity | undefined { return this.userIdentity; } isInitialized(): boolean { return this.initialized; } async testPeerIdGeneration(): Promise { this.logger.debug('[IdentityManager] === TESTING PEER ID GENERATION ==='); // Test 1: Known test vector const testSeed = new Uint8Array(32).fill(1); // Simple test seed const testPublicKey = ed25519.getPublicKey(testSeed); this.logger.debug( '[IdentityManager] Test seed (all 1s):', Array.from(testSeed) .map((b) => b.toString(16).padStart(2, '0')) .join(''), ); this.logger.debug( '[IdentityManager] Test public key:', Array.from(testPublicKey) .map((b) => b.toString(16).padStart(2, '0')) .join(''), ); const testPeerId = await this.createPeerIdFromEd25519(testSeed, testPublicKey); this.logger.debug('[IdentityManager] Test peer ID:', testPeerId); this.logger.debug('[IdentityManager] Test peer ID starts with:', testPeerId.substring(0, 8)); // Test 2: Current device if (this.devices.size > 0) { const currentDevice = await this.getCurrentDevice(); this.logger.debug('[IdentityManager] Current device ID:', currentDevice.deviceId); this.logger.debug( '[IdentityManager] Current device ID starts with:', currentDevice.deviceId.substring(0, 8), ); // Regenerate peer ID from stored keys const regeneratedId = await this.createPeerIdFromEd25519( currentDevice.privateKey, currentDevice.publicKey, ); this.logger.debug('[IdentityManager] Regenerated peer ID:', regeneratedId); this.logger.debug('[IdentityManager] IDs match?:', regeneratedId === currentDevice.deviceId); } this.logger.debug('[IdentityManager] === END TEST ==='); } async reset(): Promise { // Clear all stored data await this.storage.removeItem(this.getStorageKey('mnemonic')); await this.storage.removeItem(this.getStorageKey('userId')); await this.storage.removeItem(this.getStorageKey('devices')); await this.storage.removeItem(this.getStorageKey('currentDeviceId')); // Clear memory this.masterKey = undefined; this.userIdentity = undefined; this.devices.clear(); this.currentDeviceIndex = 0; this.initialized = false; } // Private helper methods private hashPublicKey(publicKey: Uint8Array): string { const hash = sha256(publicKey); // Convert Uint8Array to hex string without using Buffer return Array.from(hash) .map((b) => b.toString(16).padStart(2, '0')) .join('') .substring(0, 16); } private async findNextAvailableDeviceIndex(): Promise { // First check if there's a pending index from pairing const pendingIndexKey = 'identity_pending_highestDeviceIndex'; const pendingIndex = await this.storage.getItem(pendingIndexKey); if (pendingIndex !== null) { this.logger.debug( `[IdentityManager] Using pending device index from pairing: ${pendingIndex}`, ); const index = Number.parseInt(pendingIndex, 10); // Move it to the proper storage key now that we have the user ID const deviceIndexKey = this.getStorageKey('highestDeviceIndex'); await this.storage.setItem(deviceIndexKey, pendingIndex); await this.storage.removeItem(pendingIndexKey); return index; } // Try to get the highest device index from storage const deviceIndexKey = this.getStorageKey('highestDeviceIndex'); const storedIndex = await this.storage.getItem(deviceIndexKey); if (storedIndex !== null) { // Use the next index after the highest one stored const nextIndex = Number.parseInt(storedIndex, 10) + 1; await this.storage.setItem(deviceIndexKey, nextIndex.toString()); return nextIndex; } // If no stored index, check if we can detect existing devices // by trying different indices and seeing if they match our current peer ID if (this.masterKey) { try { // Get the current libp2p peer ID if available const { NativeModules } = require('react-native'); const currentPeerId = await NativeModules.Libp2pModule?.getPeerId?.(); if (currentPeerId) { // Try indices 0-9 to see if any match our current peer ID for (let i = 0; i < 10; i++) { const derivationPath = `m/44'/0'/0'/0/${i}`; const childKey = this.masterKey.derive(derivationPath); const seed = childKey.privateKey!.slice(0, 32); const keyPair = ed25519.getPublicKey(seed); const testPeerId = await this.createPeerIdFromEd25519(seed, keyPair); if (testPeerId === currentPeerId) { this.logger.debug(`[IdentityManager] Found current device at index ${i}`); // This is our current device's index, so use the next one await this.storage.setItem(deviceIndexKey, (i + 1).toString()); return i + 1; } } } } catch (e) { this.logger.debug('[IdentityManager] Could not check current peer ID:', e); } } // Default to 0 if we can't determine await this.storage.setItem(deviceIndexKey, '0'); return 0; } private async createPeerIdFromEd25519( _privateKey: Uint8Array, publicKey: Uint8Array, ): Promise { // Generate libp2p peer ID from Ed25519 public key // Swift-libp2p uses identity multihash for Ed25519 keys this.logger.debug('[IdentityManager] === PEER ID GENERATION (Ed25519) ==='); this.logger.debug('[IdentityManager] Input public key length:', publicKey.length); this.logger.debug( '[IdentityManager] Input public key (hex):', Array.from(publicKey.slice(0, 8)) .map((b) => b.toString(16).padStart(2, '0')) .join(''), ); // Based on research of swift-libp2p and the "12D3KooW" format: // 1. Ed25519 public keys use identity multihash (no hashing) // 2. The public key is prefixed with Ed25519-pub multicodec // 3. The identity multihash embeds the key directly // Ed25519-pub multicodec is 0xed (237 in decimal) // In unsigned LEB128/varint: 237 = 0xed requires 2 bytes: [0xed, 0x01] const ED25519_PUB_CODEC = new Uint8Array([0xed, 0x01]); // Build key with codec prefix const keyWithCodec = new Uint8Array(ED25519_PUB_CODEC.length + publicKey.length); keyWithCodec.set(ED25519_PUB_CODEC, 0); keyWithCodec.set(publicKey, ED25519_PUB_CODEC.length); // Create identity multihash // Format: [hash-code, length, data] // For identity: code = 0x00, followed by length, then the data const multihash = new Uint8Array(2 + keyWithCodec.length); multihash[0] = 0x00; // Identity hash function code multihash[1] = keyWithCodec.length; // Should be 34 (0x22) for Ed25519 multihash.set(keyWithCodec, 2); // Base58btc encode the multihash const peerId = this.base58Encode(multihash); this.logger.debug('[IdentityManager] Using identity multihash with Ed25519 codec'); this.logger.debug('[IdentityManager] Codec + key length:', keyWithCodec.length); this.logger.debug( '[IdentityManager] Multihash bytes:', Array.from(multihash.slice(0, 8)) .map((b) => `0x${b.toString(16).padStart(2, '0')}`) .join(' '), ); this.logger.debug('[IdentityManager] Generated peer ID:', peerId); this.logger.debug('[IdentityManager] First 8 chars:', peerId.substring(0, 8)); // The generated format should be: // - "14..." if using identity with Ed25519 codec (what we expect) // - "12D3KooW..." is what swift-libp2p actually generates // Since swift-libp2p generates a different format, we'll need to update // with the actual peer ID after libp2p starts if (!peerId.startsWith('12D3KooW')) { this.logger.debug('[IdentityManager] NOTE: Generated format differs from swift-libp2p'); this.logger.debug('[IdentityManager] Will be updated when libp2p starts with actual peer ID'); // IMPORTANT: For now, we return the identity multihash format // The App.tsx updateDevicePeerId will update this with the actual // peer ID that swift-libp2p generates when it starts } return peerId; } private base58Encode(bytes: Uint8Array): string { // Base58 alphabet (Bitcoin/IPFS format) const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; // Handle special case of all zeros if (bytes.every((b) => b === 0)) { return '1'.repeat(bytes.length); } // Convert bytes to big integer let num = 0n; for (const byte of bytes) { num = num * 256n + BigInt(byte); } // Convert to base58 let encoded = ''; while (num > 0n) { const remainder = num % 58n; num = num / 58n; encoded = ALPHABET[Number(remainder)] + encoded; } // Handle leading zeros - each leading zero byte becomes '1' in base58 let leadingZeros = 0; for (const byte of bytes) { if (byte === 0) { leadingZeros++; } else { break; } } encoded = '1'.repeat(leadingZeros) + encoded; return encoded; } private async defaultDeviceNameProvider(): Promise { // Default implementation // In React Native, this would use DeviceInfo.getDeviceName() return 'Mobile Device'; } private getStorageKey(key: string): string { return `${this.config.storagePrefix}:${key}`; } private async loadIdentity(): Promise { try { const mnemonic = await this.storage.getItem(this.getStorageKey('mnemonic')); if (!mnemonic) return null; // Restore from mnemonic await this.restoreFromMnemonic(mnemonic); // Load devices const devicesJson = await this.storage.getItem(this.getStorageKey('devices')); if (devicesJson) { const devicesList = JSON.parse(devicesJson); for (const device of devicesList) { // Convert arrays back to Uint8Array device.publicKey = new Uint8Array(device.publicKey); device.privateKey = new Uint8Array(device.privateKey); this.devices.set(device.deviceId, device); // Update device index const match = device.derivationPath.match(/\/(\d+)$/); if (match) { const index = Number.parseInt(match[1], 10); if (index >= this.currentDeviceIndex) { this.currentDeviceIndex = index + 1; } } } } return this.userIdentity!; } catch (error) { this.logger.error('Failed to load identity:', error); return null; } } private async saveIdentity(): Promise { if (!this.userIdentity) return; try { // Save mnemonic (should be encrypted in production!) if (this.userIdentity.mnemonic) { await this.storage.setItem(this.getStorageKey('mnemonic'), this.userIdentity.mnemonic); } // Save user ID await this.storage.setItem(this.getStorageKey('userId'), this.userIdentity.userId); } catch (error) { this.logger.error('Failed to save identity:', error); throw error; } } private async saveDevices(): Promise { try { const devicesList = Array.from(this.devices.values()).map((device) => ({ ...device, // Convert Uint8Array to array for JSON serialization publicKey: Array.from(device.publicKey), privateKey: Array.from(device.privateKey), })); await this.storage.setItem(this.getStorageKey('devices'), JSON.stringify(devicesList)); } catch (error) { this.logger.error('Failed to save devices:', error); throw error; } } }