Initial commit: Identity management component

- Hierarchical user/device identity system with HD key derivation
- Dependency injection for AsyncStorage and Platform
- Self-contained TypeScript declarations
- Ed25519 keypairs managed by IdentityManager
- Deterministic peer ID generation from BIP39 mnemonic
This commit is contained in:
Chris Daßler
2025-08-29 14:17:39 +02:00
commit a6b10428fa
9 changed files with 2035 additions and 0 deletions

237
.gitignore vendored Normal file
View File

@@ -0,0 +1,237 @@
# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode,node
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,visualstudiocode,node
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode,node

118
IdentityFactory.ts Normal file
View File

@@ -0,0 +1,118 @@
/**
* Factory for creating identity manager instances
*/
import { LoggerComponent } from 'ior:gitea:gitea.metatrom.net:universal-components/logger@1.0.0';
import { AsyncStorageAdapter } from './implementations/AsyncStorageAdapter';
import { IdentityManager } from './implementations/IdentityManager';
import type {
IdentityConfig,
IIdentityFactory,
IIdentityManager,
IIdentityStorage,
} from './interfaces/IIdentity';
// Platform interface for dependency injection
interface IPlatform {
OS: string;
}
export class IdentityFactory implements IIdentityFactory {
private static instance?: IdentityFactory;
private identityManager?: IIdentityManager;
private storage?: IIdentityStorage;
private logger = new LoggerComponent('IdentityFactory');
private platform?: IPlatform;
private asyncStorage?: any;
private constructor(platform?: IPlatform, asyncStorage?: any) {
this.platform = platform;
this.asyncStorage = asyncStorage;
}
static getInstance(platform?: IPlatform, asyncStorage?: any): IdentityFactory {
if (!IdentityFactory.instance) {
IdentityFactory.instance = new IdentityFactory(platform, asyncStorage);
}
return IdentityFactory.instance;
}
create(config?: IdentityConfig): IIdentityManager {
// Use singleton pattern for identity manager
if (!this.identityManager) {
const storage = this.getStorage();
// Enhanced config with platform-specific device name provider
const enhancedConfig: IdentityConfig = {
...config,
deviceNameProvider: config?.deviceNameProvider || this.getDeviceNameProvider(),
};
this.identityManager = new IdentityManager(storage, enhancedConfig);
}
return this.identityManager;
}
private getStorage(): IIdentityStorage {
if (!this.storage) {
if (!this.asyncStorage) {
throw new Error('AsyncStorage not provided. Please provide AsyncStorage when creating the factory.');
}
// Use AsyncStorage for React Native
this.storage = new AsyncStorageAdapter(this.asyncStorage);
}
return this.storage;
}
private getDeviceNameProvider(): () => Promise<string> {
return async () => {
try {
// Try to use react-native-device-info if available
const DeviceInfo = require('react-native-device-info').default;
if (DeviceInfo?.getDeviceName) {
const name = await DeviceInfo.getDeviceName();
return name || this.getFallbackDeviceName();
}
} catch (_error) {
// Module not available, use fallback
this.logger.debug('react-native-device-info not available, using fallback device name');
}
return this.getFallbackDeviceName();
};
}
private getFallbackDeviceName(): string {
const platform = this.platform?.OS || 'unknown';
const timestamp = Date.now().toString(36).substring(-4);
switch (platform) {
case 'ios':
return `iPhone-${timestamp}`;
case 'android':
return `Android-${timestamp}`;
default:
return `Device-${timestamp}`;
}
}
/**
* Reset the singleton instance (useful for testing)
*/
static reset(): void {
if (IdentityFactory.instance?.identityManager) {
IdentityFactory.instance.identityManager = undefined;
}
IdentityFactory.instance = undefined;
}
}
// Export singleton instance creator with dependency injection
export const createIdentityManager = (
config?: IdentityConfig,
platform?: IPlatform,
asyncStorage?: any
): IIdentityManager => {
return IdentityFactory.getInstance(platform, asyncStorage).create(config);
};

234
README.md Normal file
View File

@@ -0,0 +1,234 @@
# Identity Management Component v1.0.0
Hierarchical user/device identity management system with HD key derivation for multi-device support.
## Features
- **HD Key Derivation**: Uses BIP39 mnemonics and BIP32 HD wallets
- **User/Device Hierarchy**: Master user identity with derived device identities
- **Deterministic Peer IDs**: Generates standard libp2p peer IDs (12D3KooW format) from HD keys
- **Secure Storage**: Persistent storage with AsyncStorage adapter
- **Device Management**: Add, remove, and list devices with unique indices
- **Recovery**: 24-word mnemonic phrase for backup/restore
- **libp2p Compatible**: Generates Ed25519 keypairs and standard peer IDs
- **Verifiable Identity**: Peer IDs can be cryptographically verified as belonging to a user
## Usage
### Basic Usage
```typescript
import { identityManager } from '@/components/identity/1.0.0';
// Initialize identity (creates new or loads existing)
const userIdentity = await identityManager.initialize();
console.log('User ID:', userIdentity.userId);
// Get current device
const device = await identityManager.getCurrentDevice();
console.log('Device ID:', device.deviceId);
// Get libp2p keypair
const keypair = await identityManager.getLibp2pKeypair();
```
### Advanced Usage with Dependency Injection
```typescript
import {
createIdentityManager,
IdentityConfig
} from '@/components/identity/1.0.0';
const config: IdentityConfig = {
storagePrefix: '@MyApp',
mnemonicStrength: 256,
deviceNameProvider: async () => 'Custom Device Name'
};
const identityManager = createIdentityManager(config);
```
### Device Management
```typescript
// Create new device identity (auto-increments index)
const newDevice = await identityManager.createDeviceIdentity('Tablet');
console.log('New device peer ID:', newDevice.deviceId); // 12D3KooW...
// List all devices
const devices = await identityManager.getRegisteredDevices();
// Remove a device
await identityManager.removeDevice(deviceId);
// Verify a peer belongs to this user
const peerBelongsToUser = await identityManager.verifyPeerOwnership(peerId);
```
### Backup and Recovery
```typescript
// Get recovery phrase
const mnemonic = identityManager.getMnemonic();
// Restore from mnemonic (maintains device index coordination)
await identityManager.restoreFromMnemonic(mnemonic);
// After restoration, device gets next available index
const device = await identityManager.getCurrentDevice();
console.log('Device index:', device.derivationPath); // e.g., m/44'/0'/0'/0/1
```
## Interfaces
### IIdentityManager
Main interface for identity management operations.
### UserIdentity
```typescript
interface UserIdentity {
userId: string; // Hash of master public key
publicKey: Uint8Array; // Master public key
mnemonic?: string; // Recovery phrase
}
```
### DeviceIdentity
```typescript
interface DeviceIdentity {
deviceId: string; // libp2p peer ID (12D3KooW... format)
deviceName: string; // Human-readable name
publicKey: Uint8Array; // Ed25519 public key
privateKey: Uint8Array; // Ed25519 private key (seed)
derivationPath: string; // HD derivation path (m/44'/0'/0'/0/index)
createdAt: number;
lastSeen?: number;
}
```
## Architecture
```
┌─────────────────────────────────┐
│ IdentityFactory │
│ (Singleton Factory) │
└─────────────┬───────────────────┘
┌─────────────────────────────────┐
│ IdentityManager │
│ (Core Implementation) │
└─────────────┬───────────────────┘
┌─────────────────────────────────┐
│ AsyncStorageAdapter │
│ (Persistence Layer) │
└─────────────────────────────────┘
```
## HD Key Derivation Path
The component uses BIP44-like derivation paths:
```
m/44'/0'/0'/0/[device_index]
```
- `44'`: Purpose (identity management)
- `0'`: Coin type (custom for this app)
- `0'`: Account (always 0 for now)
- `0`: External chain
- `[device_index]`: Sequential device index (0, 1, 2, ...)
Each device gets a unique index, ensuring unique peer IDs while maintaining the cryptographic relationship to the user identity.
## Peer ID Generation
The system generates standard libp2p peer IDs from Ed25519 keys:
1. **Derive Ed25519 keypair** from HD path
2. **Add multicodec prefix** (0xed01) to public key
3. **Hash with SHA256** for standard format
4. **Encode as base58** with multihash prefix (0x12)
5. **Result**: Peer IDs starting with `12D3KooW...`
This ensures compatibility with all libp2p implementations while maintaining deterministic generation from the mnemonic.
## Device Index Coordination
During device pairing:
1. **Master device** tracks the highest device index
2. **New device** receives its assigned index during pairing
3. **Storage key**: `identity_<userId>_highestDeviceIndex`
4. **Fallback**: System can detect existing indices by testing derivations
This prevents peer ID collisions when multiple devices join the same identity.
## Security Considerations
1. **Mnemonic Storage**: The mnemonic is stored in AsyncStorage. In production, consider encrypting it.
2. **Private Keys**: Device private keys are stored locally. Use secure storage in production.
3. **Recovery Phrase**: Users should backup their recovery phrase securely.
4. **Device Removal**: Removing a device only removes it from the registry, not from the device itself.
5. **Peer ID Verification**: Any peer ID can be verified as belonging to a user by testing derivation paths.
6. **Deterministic Generation**: Lost devices can be recovered with the same peer ID using mnemonic + index.
## DHT Integration
The identity system integrates with the DHT for user/device discovery:
1. User identity is registered with the DHT
2. Each device is registered under the user
3. DHT tracks online/offline status of devices
4. Enables device handoff and multi-device sync
## Peer ID Update Broadcasting
When a device's peer ID changes (typically after pairing and identity restoration), the system automatically notifies other devices:
### How It Works
1. **Detection**: System detects peer ID change after `restoreFromMnemonic()`
2. **Broadcast**: Sends update notification via `/metatrom/identity/peer-update/1.0.0` protocol
3. **Update**: Other devices update their records automatically
4. **Migration**: Chat messages and connections migrate to new peer ID
### Usage
```typescript
// Automatic during pairing
const oldPeerId = await identityManager.getCurrentDevice()?.deviceId;
await identityManager.restoreFromMnemonic(mnemonic);
const newPeerId = await identityManager.getCurrentDevice()?.deviceId;
// Broadcast if changed (automatic in pairing flow)
if (oldPeerId !== newPeerId) {
await identityManager.broadcastPeerIdUpdate(
oldPeerId,
newPeerId,
sendProtocolData
);
}
```
### Benefits
- **Seamless Updates**: No manual intervention needed
- **Preserves History**: Chat and connection history maintained
- **Prevents Duplicates**: Eliminates duplicate peer ID issues
- **Network Coherence**: All devices stay synchronized
## Version
- Component Version: 1.0.0
- Protocol Version: /metatrom/identity/1.0.0
## License
MIT

View File

@@ -0,0 +1,43 @@
/**
* AsyncStorage adapter for React Native
*/
import type { IIdentityStorage } from '../interfaces/IIdentity';
// AsyncStorage type definition for dependency injection
interface IAsyncStorage {
setItem(key: string, value: string): Promise<void>;
getItem(key: string): Promise<string | null>;
removeItem(key: string): Promise<void>;
getAllKeys(): Promise<string[]>;
multiRemove(keys: string[]): Promise<void>;
}
export class AsyncStorageAdapter implements IIdentityStorage {
private asyncStorage: IAsyncStorage;
constructor(asyncStorage: IAsyncStorage) {
this.asyncStorage = asyncStorage;
}
async setItem(key: string, value: string): Promise<void> {
await this.asyncStorage.setItem(key, value);
}
async getItem(key: string): Promise<string | null> {
return await this.asyncStorage.getItem(key);
}
async removeItem(key: string): Promise<void> {
await this.asyncStorage.removeItem(key);
}
async clear(): Promise<void> {
// Only clear identity-related keys
const keys = await this.asyncStorage.getAllKeys();
const identityKeys = keys.filter((key) => key.startsWith('@IdentityManager'));
if (identityKeys.length > 0) {
await this.asyncStorage.multiRemove(identityKeys);
}
}
}

View File

@@ -0,0 +1,937 @@
/**
* 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<string, DeviceIdentity> = new Map();
private currentDeviceIndex: number = 0;
private storage: IIdentityStorage;
private config: Required<IdentityConfig>;
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<UserIdentity> {
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<UserIdentity> {
// 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<void>,
): Promise<void> {
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<UserIdentity> {
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<DeviceIdentity> {
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<string> {
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<DeviceIdentity> {
// 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<DeviceIdentity[]> {
return Array.from(this.devices.values());
}
async removeDevice(deviceId: string): Promise<void> {
// 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<number> {
// 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<string, DeviceIdentity[]>();
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<number> {
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<void> {
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<void> {
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<void> {
// 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<number> {
// 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<string> {
// 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<string> {
// 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<UserIdentity | null> {
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<void> {
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<void> {
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;
}
}
}

234
index.d.ts vendored Normal file
View File

@@ -0,0 +1,234 @@
declare module '@metatrom/identity' {
/**
* User identity (master identity)
*/
export interface UserIdentity {
userId: string; // Hash of master public key
publicKey: Uint8Array; // Master public key
mnemonic?: string; // Stored securely, only on this device
}
/**
* Device identity (derived from user)
*/
export interface DeviceIdentity {
deviceId: string; // libp2p peer ID
deviceName: string; // Human-readable name
publicKey: Uint8Array; // Device public key (Ed25519)
privateKey: Uint8Array; // Device private key (Ed25519)
derivationPath: string; // HD derivation path
createdAt: number;
lastSeen?: number;
}
/**
* Device information for remote devices
*/
export interface DeviceInfo {
deviceId: string;
deviceName: string;
multiaddrs: string[];
isOnline: boolean;
lastSeen: number;
}
/**
* Configuration for identity manager
*/
export interface IdentityConfig {
storagePrefix?: string;
mnemonicStrength?: 128 | 160 | 192 | 224 | 256;
deviceNameProvider?: () => Promise<string>;
}
/**
* Main identity management interface
*/
export interface IIdentityManager {
/**
* Initialize the identity manager
* Either loads existing identity or creates new one
*/
initialize(): Promise<UserIdentity>;
/**
* Create a completely new user identity with mnemonic
*/
createNewIdentity(): Promise<UserIdentity>;
/**
* Restore identity from mnemonic phrase
*/
restoreFromMnemonic(mnemonic: string): Promise<UserIdentity>;
/**
* Create a new device identity derived from master
*/
createDeviceIdentity(deviceName: string): Promise<DeviceIdentity>;
/**
* Get current device identity or create one if none exists
*/
getCurrentDevice(): Promise<DeviceIdentity>;
/**
* List all registered devices for this user
*/
getRegisteredDevices(): Promise<DeviceIdentity[]>;
/**
* Remove a device from the user's identity
*/
removeDevice(deviceId: string): Promise<void>;
/**
* Clean up duplicate devices with the same base name
* @returns Number of devices removed
*/
cleanupDuplicateDevices(): Promise<number>;
/**
* Clean up inactive devices (except current device)
* @param daysInactive - Number of days of inactivity before removal (default: 30)
* @returns Number of devices removed
*/
cleanupInactiveDevices(daysInactive?: number): Promise<number>;
/**
* Get the libp2p keypair for current device
*/
getLibp2pKeypair(): Promise<{ privateKey: Uint8Array; publicKey: Uint8Array }>;
/**
* Update the current device's peer ID after libp2p generates it
*/
updateDevicePeerId(peerId: string): Promise<void>;
/**
* Calculate what the peer ID will be for a given device index
* Useful during pairing to predict the new device's peer ID
*/
calculatePeerIdForIndex(deviceIndex: number): Promise<string>;
/**
* Export mnemonic for backup (should be done securely!)
*/
getMnemonic(): string | undefined;
/**
* Get user identity
*/
getUserIdentity(): UserIdentity | undefined;
/**
* Check if identity is initialized
*/
isInitialized(): boolean;
/**
* Clear all identity data (dangerous!)
*/
reset(): Promise<void>;
/**
* Broadcast peer ID update to other devices
*/
broadcastPeerIdUpdate(
oldPeerId: string,
newPeerId: string,
sendProtocolData?: (peerId: string, protocolId: string, data: Uint8Array) => Promise<void>,
): Promise<void>;
}
/**
* Storage interface for identity persistence
*/
export interface IIdentityStorage {
setItem(key: string, value: string): Promise<void>;
getItem(key: string): Promise<string | null>;
removeItem(key: string): Promise<void>;
clear(): Promise<void>;
}
/**
* Factory interface for creating identity managers
*/
export interface IIdentityFactory {
create(config?: IdentityConfig): IIdentityManager;
}
// AsyncStorage type definition for dependency injection
interface IAsyncStorage {
setItem(key: string, value: string): Promise<void>;
getItem(key: string): Promise<string | null>;
removeItem(key: string): Promise<void>;
getAllKeys(): Promise<string[]>;
multiRemove(keys: string[]): Promise<void>;
}
// Platform interface for dependency injection
interface IPlatform {
OS: string;
}
/**
* AsyncStorage adapter for React Native
*/
export class AsyncStorageAdapter implements IIdentityStorage {
constructor(asyncStorage: IAsyncStorage);
setItem(key: string, value: string): Promise<void>;
getItem(key: string): Promise<string | null>;
removeItem(key: string): Promise<void>;
clear(): Promise<void>;
}
/**
* Identity Manager Implementation
*/
export class IdentityManager implements IIdentityManager {
constructor(storage: IIdentityStorage, config?: IdentityConfig);
initialize(): Promise<UserIdentity>;
createNewIdentity(): Promise<UserIdentity>;
restoreFromMnemonic(mnemonic: string): Promise<UserIdentity>;
createDeviceIdentity(deviceName: string): Promise<DeviceIdentity>;
getCurrentDevice(): Promise<DeviceIdentity>;
getRegisteredDevices(): Promise<DeviceIdentity[]>;
removeDevice(deviceId: string): Promise<void>;
cleanupDuplicateDevices(): Promise<number>;
cleanupInactiveDevices(daysInactive?: number): Promise<number>;
getLibp2pKeypair(): Promise<{ privateKey: Uint8Array; publicKey: Uint8Array }>;
updateDevicePeerId(peerId: string): Promise<void>;
calculatePeerIdForIndex(deviceIndex: number): Promise<string>;
getMnemonic(): string | undefined;
getUserIdentity(): UserIdentity | undefined;
isInitialized(): boolean;
reset(): Promise<void>;
broadcastPeerIdUpdate(
oldPeerId: string,
newPeerId: string,
sendProtocolData?: (peerId: string, protocolId: string, data: Uint8Array) => Promise<void>,
): Promise<void>;
}
/**
* Factory for creating identity manager instances
*/
export class IdentityFactory implements IIdentityFactory {
static getInstance(platform?: IPlatform, asyncStorage?: IAsyncStorage): IdentityFactory;
create(config?: IdentityConfig): IIdentityManager;
static reset(): void;
}
/**
* Create identity manager with dependency injection
*/
export function createIdentityManager(
config?: IdentityConfig,
platform?: IPlatform,
asyncStorage?: IAsyncStorage
): IIdentityManager;
// Version information
export const VERSION: string;
export const PROTOCOL_VERSION: string;
}

31
index.ts Normal file
View File

@@ -0,0 +1,31 @@
/**
* Identity Management Component
*
* Provides hierarchical user/device identity management with HD key derivation
*
* @module identity/1.0.0
*/
// Export factory and helper
export { createIdentityManager, IdentityFactory } from './IdentityFactory';
export { AsyncStorageAdapter } from './implementations/AsyncStorageAdapter';
// Export implementations (for advanced usage)
export { IdentityManager } from './implementations/IdentityManager';
// Export interfaces
export type {
DeviceIdentity,
DeviceInfo,
IdentityConfig,
IIdentityFactory,
IIdentityManager,
IIdentityStorage,
UserIdentity,
} from './interfaces/IIdentity';
// Note: Default singleton instance removed - must be created with dependencies
// Use createIdentityManager() with platform and storage dependencies
// Version information
export const VERSION = '1.0.0';
export const PROTOCOL_VERSION = '/metatrom/identity/1.0.0';

163
interfaces/IIdentity.ts Normal file
View File

@@ -0,0 +1,163 @@
/**
* Identity Management Interfaces
*
* Provides hierarchical user/device identity management with HD key derivation
*/
/**
* User identity (master identity)
*/
export interface UserIdentity {
userId: string; // Hash of master public key
publicKey: Uint8Array; // Master public key
mnemonic?: string; // Stored securely, only on this device
}
/**
* Device identity (derived from user)
*/
export interface DeviceIdentity {
deviceId: string; // libp2p peer ID
deviceName: string; // Human-readable name
publicKey: Uint8Array; // Device public key (Ed25519)
privateKey: Uint8Array; // Device private key (Ed25519)
derivationPath: string; // HD derivation path
createdAt: number;
lastSeen?: number;
}
/**
* Device information for remote devices
*/
export interface DeviceInfo {
deviceId: string;
deviceName: string;
multiaddrs: string[];
isOnline: boolean;
lastSeen: number;
}
/**
* Configuration for identity manager
*/
export interface IdentityConfig {
storagePrefix?: string;
mnemonicStrength?: 128 | 160 | 192 | 224 | 256;
deviceNameProvider?: () => Promise<string>;
}
/**
* Main identity management interface
*/
export interface IIdentityManager {
/**
* Initialize the identity manager
* Either loads existing identity or creates new one
*/
initialize(): Promise<UserIdentity>;
/**
* Create a completely new user identity with mnemonic
*/
createNewIdentity(): Promise<UserIdentity>;
/**
* Restore identity from mnemonic phrase
*/
restoreFromMnemonic(mnemonic: string): Promise<UserIdentity>;
/**
* Create a new device identity derived from master
*/
createDeviceIdentity(deviceName: string): Promise<DeviceIdentity>;
/**
* Get current device identity or create one if none exists
*/
getCurrentDevice(): Promise<DeviceIdentity>;
/**
* List all registered devices for this user
*/
getRegisteredDevices(): Promise<DeviceIdentity[]>;
/**
* Remove a device from the user's identity
*/
removeDevice(deviceId: string): Promise<void>;
/**
* Clean up duplicate devices with the same base name
* @returns Number of devices removed
*/
cleanupDuplicateDevices(): Promise<number>;
/**
* Clean up inactive devices (except current device)
* @param daysInactive - Number of days of inactivity before removal (default: 30)
* @returns Number of devices removed
*/
cleanupInactiveDevices(daysInactive?: number): Promise<number>;
/**
* Get the libp2p keypair for current device
*/
getLibp2pKeypair(): Promise<{ privateKey: Uint8Array; publicKey: Uint8Array }>;
/**
* Update the current device's peer ID after libp2p generates it
*/
updateDevicePeerId(peerId: string): Promise<void>;
/**
* Calculate what the peer ID will be for a given device index
* Useful during pairing to predict the new device's peer ID
*/
calculatePeerIdForIndex(deviceIndex: number): Promise<string>;
/**
* Export mnemonic for backup (should be done securely!)
*/
getMnemonic(): string | undefined;
/**
* Get user identity
*/
getUserIdentity(): UserIdentity | undefined;
/**
* Check if identity is initialized
*/
isInitialized(): boolean;
/**
* Clear all identity data (dangerous!)
*/
reset(): Promise<void>;
/**
* Broadcast peer ID update to other devices
*/
broadcastPeerIdUpdate(
oldPeerId: string,
newPeerId: string,
sendProtocolData?: (peerId: string, protocolId: string, data: Uint8Array) => Promise<void>,
): Promise<void>;
}
/**
* Storage interface for identity persistence
*/
export interface IIdentityStorage {
setItem(key: string, value: string): Promise<void>;
getItem(key: string): Promise<string | null>;
removeItem(key: string): Promise<void>;
clear(): Promise<void>;
}
/**
* Factory for creating identity manager instances
*/
export interface IIdentityFactory {
create(config?: IdentityConfig): IIdentityManager;
}

38
package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "@metatrom/identity",
"version": "1.0.0",
"main": "index.ts",
"type": "module",
"description": "Identity management system with hierarchical user-device structure",
"metatrom": {
"ior": "com.metatrom.identity@1.0.0",
"capabilities": {
"p2p": true,
"contracts": false,
"viewer": false,
"sync": true
}
},
"exports": {
".": "./index.ts",
"./interfaces": "./interfaces/IIdentity.ts",
"./implementations": "./implementations/index.ts"
},
"dependencies": {
"@noble/curves": "^1.9.6",
"@noble/hashes": "^1.8.0",
"@scure/bip32": "^1.7.0",
"bip39": "^3.1.0"
},
"peerDependencies": {
"@react-native-async-storage/async-storage": "^2.2.0"
},
"keywords": [
"metatrom",
"identity",
"p2p",
"ed25519",
"hd-keys"
],
"license": "MIT"
}