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:
237
.gitignore
vendored
Normal file
237
.gitignore
vendored
Normal 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
118
IdentityFactory.ts
Normal 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
234
README.md
Normal 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
|
||||
43
implementations/AsyncStorageAdapter.ts
Normal file
43
implementations/AsyncStorageAdapter.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
937
implementations/IdentityManager.ts
Normal file
937
implementations/IdentityManager.ts
Normal 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
234
index.d.ts
vendored
Normal 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
31
index.ts
Normal 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
163
interfaces/IIdentity.ts
Normal 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
38
package.json
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user