me2em-protocol-monorepo 0.5.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +0 -0
- package/CODE_OF_CONDUCT.md +73 -0
- package/CONTRIBUTING.md +145 -0
- package/GOVERNANCE.md +132 -0
- package/LICENSE +201 -0
- package/README.md +870 -0
- package/SECURITY.md +70 -0
- package/package.json +20 -0
- package/packages/core/README.md +750 -0
- package/packages/core/package.json +45 -0
- package/packages/core/src/crypto/ed25519.ts +12 -0
- package/packages/core/src/crypto/hkdf.ts +22 -0
- package/packages/core/src/crypto/index.ts +2 -0
- package/packages/core/src/crypto/init.ts +18 -0
- package/packages/core/src/handle.ts +167 -0
- package/packages/core/src/identity.ts +89 -0
- package/packages/core/src/index.ts +35 -0
- package/packages/core/src/seed.ts +138 -0
- package/packages/core/src/session.ts +241 -0
- package/packages/core/test/derivation.spec.ts +117 -0
- package/packages/core/test/session.spec.ts +335 -0
- package/packages/core/tsconfig.json +9 -0
- package/pnpm-workspace.yaml +2 -0
- package/specs/core.md +44 -0
- package/specs/openapi/server-ref.yaml +10 -0
- package/specs/test-vectors.json +13 -0
- package/tsconfig.base.json +20 -0
- package/typedoc.json +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
# π¦ `@me2em/core` β Developer Documentation
|
|
2
|
+
|
|
3
|
+
> Core primitives for the Me2em authorization protocol: `Identity`, `Handle`, and `Session` management with Ed25519 cryptography.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@me2em/core)
|
|
6
|
+
[](https://github.com/me2em-org/me2em-protocol/blob/main/LICENSE)
|
|
7
|
+
[](https://docs.me2em.com)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## π― Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @me2em/core
|
|
15
|
+
# or
|
|
16
|
+
pnpm add @me2em/core
|
|
17
|
+
# or
|
|
18
|
+
yarn add @me2em/core
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Identity, Handle } from '@me2em/core';
|
|
23
|
+
|
|
24
|
+
// 1. Create or restore identity from seed
|
|
25
|
+
const seed = 'your-32-byte-hex-seed-or-24-words';
|
|
26
|
+
const identity = await Identity.fromSeed(seed);
|
|
27
|
+
|
|
28
|
+
// 2. Derive a contextual handle
|
|
29
|
+
const workHandle = await identity.deriveHandle('work', {
|
|
30
|
+
displayName: 'Alice @ Work',
|
|
31
|
+
avatar: 'https://example.com/avatar.png'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 3. Use handle for cryptographic operations
|
|
35
|
+
const message = new TextEncoder().encode('Hello, world!');
|
|
36
|
+
const signature = await workHandle.sign(message);
|
|
37
|
+
const isValid = await Handle.verify(signature, message, workHandle.getPublicKey());
|
|
38
|
+
|
|
39
|
+
console.log('Handle ID:', workHandle.getId()); // base64url(publicKey)
|
|
40
|
+
console.log('Signature valid:', isValid); // true
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## π Table of Contents
|
|
46
|
+
|
|
47
|
+
1. [Core Concepts](#-core-concepts)
|
|
48
|
+
2. [API Reference](#-api-reference)
|
|
49
|
+
3. [Use Cases](#-use-cases)
|
|
50
|
+
4. [Cryptographic Details](#-cryptographic-details)
|
|
51
|
+
5. [Integration Guide](#-integration-guide)
|
|
52
|
+
6. [Security Best Practices](#-security-best-practices)
|
|
53
|
+
7. [Testing](#-testing)
|
|
54
|
+
8. [Contributing](#-contributing)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## π Core Concepts
|
|
59
|
+
|
|
60
|
+
### Identity β Cryptographic Root
|
|
61
|
+
|
|
62
|
+
`Identity` represents the root cryptographic identity derived from a seed phrase.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Seed (32 bytes / 24 words)
|
|
66
|
+
β
|
|
67
|
+
βΌ
|
|
68
|
+
βββββββββββββββββββ
|
|
69
|
+
β Identity β
|
|
70
|
+
β β’ Ed25519 key β
|
|
71
|
+
β β’ Deterministic β
|
|
72
|
+
β β’ Client-only β
|
|
73
|
+
βββββββββββββββββββ
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Key properties:**
|
|
77
|
+
- π **Never transmitted**: The root private key never leaves the client device
|
|
78
|
+
- π **Deterministic**: Same seed β same Identity β same derived Handles
|
|
79
|
+
- π§© **Stateless**: No server storage required for cryptographic operations
|
|
80
|
+
|
|
81
|
+
### Handle β Contextual Profile
|
|
82
|
+
|
|
83
|
+
`Handle` is a derived Ed25519 keypair representing a specific context (work, personal, IoT device).
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
Identity
|
|
87
|
+
β
|
|
88
|
+
ββ deriveHandle('work') β Handle(@alice_work)
|
|
89
|
+
ββ deriveHandle('private') β Handle(@alice_private)
|
|
90
|
+
ββ deriveHandle('iot-device-001') β Handle(@device_001)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Key properties:**
|
|
94
|
+
- π **Cryptographically isolated**: Each Handle has its own keypair; compromise of one does not affect others
|
|
95
|
+
- π·οΈ **Public identifier**: `Handle.getId()` returns `base64url(publicKey)` β safe to share
|
|
96
|
+
- π¦ **Metadata support**: Attach public metadata (displayName, avatar) without revealing Identity
|
|
97
|
+
|
|
98
|
+
### Session β Scoped Access Token
|
|
99
|
+
|
|
100
|
+
`Session` represents a time-limited, scoped authorization token for accessing a specific application.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
interface SessionOptions {
|
|
104
|
+
audience: string; // e.g., 'https://api.myapp.com'
|
|
105
|
+
scopes: string[]; // e.g., ['read:profile', 'write:messages']
|
|
106
|
+
ttl?: number; // seconds until expiration (default: 3600)
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Key properties:**
|
|
111
|
+
- β±οΈ **Short-lived**: Tokens expire automatically; refresh via Handle re-authentication
|
|
112
|
+
- π **Scoped**: Each token grants only specified permissions
|
|
113
|
+
- π **Stateless verification**: Servers verify signatures using Handle public key β no database lookup required
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## π API Reference
|
|
118
|
+
|
|
119
|
+
### `Identity` Class
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
class Identity {
|
|
123
|
+
// Create Identity from seed (32-byte Uint8Array or hex string)
|
|
124
|
+
static fromSeed(seed: Uint8Array | string): Promise<Identity>;
|
|
125
|
+
|
|
126
|
+
// Derive a new Handle with optional metadata
|
|
127
|
+
deriveHandle(name: string, metadata?: HandleMetadata): Promise<Handle>;
|
|
128
|
+
|
|
129
|
+
// Get root public key (for verification, never share private key)
|
|
130
|
+
getPublicKey(): Uint8Array;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### `Identity.fromSeed(seed)`
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// From hex string (64 chars = 32 bytes)
|
|
138
|
+
const identity = await Identity.fromSeed('a1b2c3...');
|
|
139
|
+
|
|
140
|
+
// From Uint8Array
|
|
141
|
+
const seedBytes = new Uint8Array(32).fill(42);
|
|
142
|
+
const identity = await Identity.fromSeed(seedBytes);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
β οΈ **Security**: The seed must be kept secret. Never log, transmit, or store it in plaintext.
|
|
146
|
+
|
|
147
|
+
#### `Identity.deriveHandle(name, metadata?)`
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const handle = await identity.deriveHandle('work', {
|
|
151
|
+
displayName: 'Alice Developer',
|
|
152
|
+
avatar: 'https://example.com/alice.png',
|
|
153
|
+
org: 'Acme Corp'
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- `name`: Unique identifier for this Handle within the Identity (case-insensitive, trimmed)
|
|
158
|
+
- `metadata`: Arbitrary public data attached to the Handle (visible to servers/other users)
|
|
159
|
+
|
|
160
|
+
Returns a `Handle` instance with its own keypair, derived deterministically from the Identity.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### `Handle` Class
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
class Handle {
|
|
168
|
+
// Get public identifier (safe to share)
|
|
169
|
+
getId(): string; // base64url-encoded public key
|
|
170
|
+
|
|
171
|
+
// Sign arbitrary data with Handle's private key
|
|
172
|
+
sign(data: Uint8Array): Promise<Uint8Array>;
|
|
173
|
+
|
|
174
|
+
// Verify a signature using a public key (static method)
|
|
175
|
+
static verify(signature: Uint8Array, data: Uint8Array, publicKey: Uint8Array): Promise<boolean>;
|
|
176
|
+
|
|
177
|
+
// Deterministically derive a password/secret for a specific context
|
|
178
|
+
derivePassword(context: string, length?: number): string;
|
|
179
|
+
|
|
180
|
+
// Derive a symmetric 256-bit channel key for encrypted communication
|
|
181
|
+
deriveChannelKey(context: string): Uint8Array;
|
|
182
|
+
|
|
183
|
+
// Accessors
|
|
184
|
+
getName(): string;
|
|
185
|
+
getMetadata(): HandleMetadata | undefined;
|
|
186
|
+
getPublicKey(): Uint8Array;
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### `Handle.getId()`
|
|
191
|
+
|
|
192
|
+
Returns a URL-safe base64 string representing the Handle's public key:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const handleId = handle.getId();
|
|
196
|
+
// Example: "pK7xJ2mN8vQ3rL5wY9zB1cD4eF6gH8iJ0kL2mN4oP6qR8sT0uV2wX4yZ6aB8cD0"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
This is the identifier you send to servers for authentication and routing.
|
|
200
|
+
|
|
201
|
+
#### `Handle.sign(data)`
|
|
202
|
+
|
|
203
|
+
Signs arbitrary binary data using Ed25519:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const payload = new TextEncoder().encode('{"action":"post","content":"Hello"}');
|
|
207
|
+
const signature = await handle.sign(payload);
|
|
208
|
+
|
|
209
|
+
// Send to server:
|
|
210
|
+
fetch('https://api.example.com/endpoint', {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: {
|
|
213
|
+
'X-Handle-ID': handle.getId(),
|
|
214
|
+
'Content-Type': 'application/json'
|
|
215
|
+
},
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
payload: btoa(String.fromCharCode(...payload)),
|
|
218
|
+
signature: btoa(String.fromCharCode(...signature))
|
|
219
|
+
})
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### `Handle.verify(signature, data, publicKey)`
|
|
224
|
+
|
|
225
|
+
Static method for server-side signature verification:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// Server receives: handleId, signature, payload
|
|
229
|
+
const publicKey = Uint8Array.from(atob(handleId), c => c.charCodeAt(0));
|
|
230
|
+
const data = Uint8Array.from(atob(payload), c => c.charCodeAt(0));
|
|
231
|
+
const sig = Uint8Array.from(atob(signature), c => c.charCodeAt(0));
|
|
232
|
+
|
|
233
|
+
const isValid = await Handle.verify(sig, data, publicKey);
|
|
234
|
+
if (!isValid) throw new Error('Invalid signature');
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### `Handle.derivePassword(context, length?)`
|
|
238
|
+
|
|
239
|
+
Generates a deterministic, cryptographically strong secret (e.g., a password) for a specific service, without ever exposing the private key.
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// Derive a password for a specific service
|
|
243
|
+
const googlePassword = workHandle.derivePassword('google');
|
|
244
|
+
// Example output: "xK9mP2qL5wY9zB1cD4eF6g"
|
|
245
|
+
|
|
246
|
+
// Derive a longer secret (e.g., 32 bytes for an API key)
|
|
247
|
+
const apiKey = workHandle.derivePassword('aws-api', 32);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
β
**Security Benefit**: The `privateKey` remains strictly encapsulated within the `Handle` instance. It is used internally by HKDF-SHA256 and is never returned, logged, or serialized.
|
|
251
|
+
|
|
252
|
+
#### `Handle.deriveChannelKey(context)`
|
|
253
|
+
|
|
254
|
+
Derives a symmetric 256-bit key for establishing a secure, encrypted communication channel between the Identity (controller) and this Handle (device/context). Both parties can independently compute this key without any key exchange protocol, because they both have access to the Handle's private key.
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// On the device (Handle side):
|
|
258
|
+
const channelKey = droneHandle.deriveChannelKey('telemetry-v1');
|
|
259
|
+
// channelKey is a 32-byte Uint8Array, ready for AES-256-GCM
|
|
260
|
+
|
|
261
|
+
// On the controller (Identity side):
|
|
262
|
+
const droneHandle = await centerIdentity.deriveHandle('drone-001');
|
|
263
|
+
const channelKey = droneHandle.deriveChannelKey('telemetry-v1');
|
|
264
|
+
// Identical key, derived independently
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
β
**Security Benefit**: The `privateKey` remains strictly encapsulated within the `Handle` instance. No key exchange protocol (ECDH, etc.) is needed β both parties derive the same key independently from the shared Handle private key.
|
|
268
|
+
|
|
269
|
+
β οΈ **Note**: This method returns raw bytes (Uint8Array), unlike `derivePassword` which returns a base64url string. Use the returned bytes directly with AES-256-GCM via Web Crypto API or similar.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### `Session` Class (Planned)
|
|
274
|
+
|
|
275
|
+
> β οΈ Session management is planned for v0.2. Current implementations should use custom token logic with Handle signatures.
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// Future API (not yet implemented)
|
|
279
|
+
const session = await handle.requestSession({
|
|
280
|
+
audience: 'https://api.myapp.com',
|
|
281
|
+
scopes: ['read:profile', 'write:messages'],
|
|
282
|
+
ttl: 3600
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Use token
|
|
286
|
+
fetch('https://api.myapp.com/me', {
|
|
287
|
+
headers: { Authorization: `Bearer ${session.token}` }
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## π― Use Cases
|
|
294
|
+
|
|
295
|
+
### 1. Multi-Context User Authentication
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// User logs in with seed
|
|
299
|
+
const identity = await Identity.fromSeed(userSeed);
|
|
300
|
+
|
|
301
|
+
// Derive work handle
|
|
302
|
+
const workHandle = await identity.deriveHandle('work', {
|
|
303
|
+
displayName: 'Alice @ Acme',
|
|
304
|
+
role: 'engineer'
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Authenticate to work app
|
|
308
|
+
const workSig = await workHandle.sign(challenge);
|
|
309
|
+
// Send workHandle.getId() + workSig to https://work.acme.com/auth
|
|
310
|
+
|
|
311
|
+
// Later, derive personal handle for social app
|
|
312
|
+
const personalHandle = await identity.deriveHandle('personal', {
|
|
313
|
+
displayName: 'Alice',
|
|
314
|
+
interests: ['hiking', 'photography']
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Authenticate to social app with different handle
|
|
318
|
+
const socialSig = await personalHandle.sign(challenge);
|
|
319
|
+
// Send personalHandle.getId() + socialSig to https://social.app/auth
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
β
**Benefit**: One seed, multiple isolated identities. Apps see only the Handle they interact with.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
### 2. IoT Device Hierarchy
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// Root identity for device fleet
|
|
330
|
+
const fleetIdentity = await Identity.fromSeed(fleetSeed);
|
|
331
|
+
|
|
332
|
+
// Derive handle for gateway
|
|
333
|
+
const gateway = await fleetIdentity.deriveHandle('gateway-001', {
|
|
334
|
+
type: 'gateway',
|
|
335
|
+
location: 'warehouse-a'
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Derive child handles for sensors (using name prefix for hierarchy)
|
|
339
|
+
const sensor1 = await fleetIdentity.deriveHandle('gateway-001/sensor-temp', {
|
|
340
|
+
type: 'temperature',
|
|
341
|
+
unit: 'celsius'
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const sensor2 = await fleetIdentity.deriveHandle('gateway-001/sensor-humidity', {
|
|
345
|
+
type: 'humidity',
|
|
346
|
+
unit: 'percent'
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
β
**Benefit**: Compromise of `sensor-temp` does not expose `gateway-001` or other sensors. Each device has isolated credentials.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
### 3. Anonymous Guest Access
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// Generate ephemeral seed for guest session
|
|
358
|
+
const guestSeed = crypto.getRandomValues(new Uint8Array(32));
|
|
359
|
+
const guestIdentity = await Identity.fromSeed(guestSeed);
|
|
360
|
+
|
|
361
|
+
// Create anonymous handle
|
|
362
|
+
const guestHandle = await guestIdentity.deriveHandle('guest', {
|
|
363
|
+
displayName: 'Anonymous User',
|
|
364
|
+
ephemeral: true
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Use for limited-scope access
|
|
368
|
+
const guestSig = await guestHandle.sign(challenge);
|
|
369
|
+
// Send to server with scope: ['read:public-content']
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
β
**Benefit**: No email, no phone, no tracking. Guest access with cryptographic accountability.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
### 4. Cross-App Single Sign-On (SSO)
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
// User authenticates once with Identity
|
|
380
|
+
const identity = await Identity.fromSeed(seed);
|
|
381
|
+
|
|
382
|
+
// App A requests session for 'app-a.example.com'
|
|
383
|
+
const handleA = await identity.deriveHandle('app-a', { app: 'example-a' });
|
|
384
|
+
const sessionA = { /* custom token logic */ };
|
|
385
|
+
|
|
386
|
+
// App B requests session for 'app-b.example.com'
|
|
387
|
+
const handleB = await identity.deriveHandle('app-b', { app: 'example-b' });
|
|
388
|
+
const sessionB = { /* custom token logic */ };
|
|
389
|
+
|
|
390
|
+
// User switches between apps without re-entering seed
|
|
391
|
+
// Each app sees only its Handle, not the others
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
β
**Benefit**: Seamless SSO with privacy isolation β apps cannot correlate user activity across services.
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
### 5. Deterministic Password Manager (Access Key Keeper)
|
|
399
|
+
|
|
400
|
+
Instead of storing passwords in a database, derive them deterministically from the Handle. The user only needs to remember their root Seed and the service name.
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// 1. User restores Identity from Seed (e.g., after entering a PIN)
|
|
404
|
+
const identity = await Identity.fromSeed(userSeed);
|
|
405
|
+
|
|
406
|
+
// 2. Derive the specific service Handle
|
|
407
|
+
const googleHandle = await identity.deriveHandle('google', {
|
|
408
|
+
displayName: 'Alice Personal'
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// 3. Deterministically generate the password on the fly
|
|
412
|
+
const password = googleHandle.derivePassword('google');
|
|
413
|
+
|
|
414
|
+
// 4. Auto-fill the login form
|
|
415
|
+
console.log('Login:', 'alice@example.com');
|
|
416
|
+
console.log('Password:', password); // Always the same for this seed + handle + context
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
β
**Benefit**: Zero-knowledge password management. No database of passwords is required on the server. If a service forces a password change, the user simply derives a new handle (e.g., `'google-v2'`) or adds a version suffix to the context (e.g., `derivePassword('google', 16, 2)`).
|
|
420
|
+
|
|
421
|
+
### 6. IoT Fleet with Encrypted Channels
|
|
422
|
+
|
|
423
|
+
Control a fleet of devices (drones, sensors, robots) with zero-knowledge encrypted communication. The control center derives a channel key for each device, and the device independently derives the same key β no key exchange protocol required.
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
// === CONTROL CENTER (Identity) ===
|
|
427
|
+
const centerIdentity = await Identity.fromSeed(centerSeed);
|
|
428
|
+
|
|
429
|
+
// Minimal registry: just device names (no public keys stored!)
|
|
430
|
+
const allowedDevices = ['drone-001', 'drone-002', 'sensor-warehouse-a'];
|
|
431
|
+
|
|
432
|
+
// Receiving telemetry from a drone
|
|
433
|
+
async function receiveTelemetry(message: { name: string, signature: Uint8Array, encrypted: Uint8Array }) {
|
|
434
|
+
// 1. Check if device is in registry
|
|
435
|
+
if (!allowedDevices.includes(message.name)) {
|
|
436
|
+
throw new Error('Unknown device');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 2. Derive the Handle (deterministic, no DB lookup)
|
|
440
|
+
const deviceHandle = await centerIdentity.deriveHandle(message.name);
|
|
441
|
+
|
|
442
|
+
// 3. Verify signature (proves device owns the private key)
|
|
443
|
+
const dataToVerify = concatBytes(
|
|
444
|
+
new TextEncoder().encode(message.name),
|
|
445
|
+
message.encrypted
|
|
446
|
+
);
|
|
447
|
+
const isValid = await Handle.verify(message.signature, dataToVerify, deviceHandle.getPublicKey());
|
|
448
|
+
if (!isValid) throw new Error('Invalid signature');
|
|
449
|
+
|
|
450
|
+
// 4. Derive the SAME channel key the device used for encryption
|
|
451
|
+
const channelKey = deviceHandle.deriveChannelKey('telemetry-v1');
|
|
452
|
+
|
|
453
|
+
// 5. Decrypt the message
|
|
454
|
+
const telemetry = await decryptAESGCM(message.encrypted, channelKey);
|
|
455
|
+
return telemetry;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// === DRONE (Handle, provisioned at factory) ===
|
|
459
|
+
// Drone is provisioned with its Handle's private key and name
|
|
460
|
+
async function sendTelemetry(telemetry: object) {
|
|
461
|
+
const name = 'drone-001';
|
|
462
|
+
const privateKey = /* loaded from secure enclave */;
|
|
463
|
+
const droneHandle = new Handle(privateKey, name);
|
|
464
|
+
|
|
465
|
+
// 1. Derive the SAME channel key the center will use for decryption
|
|
466
|
+
const channelKey = droneHandle.deriveChannelKey('telemetry-v1');
|
|
467
|
+
|
|
468
|
+
// 2. Encrypt telemetry
|
|
469
|
+
const telemetryBytes = new TextEncoder().encode(JSON.stringify(telemetry));
|
|
470
|
+
const encrypted = await encryptAESGCM(telemetryBytes, channelKey);
|
|
471
|
+
|
|
472
|
+
// 3. Sign (name + encrypted data)
|
|
473
|
+
const dataToSign = concatBytes(new TextEncoder().encode(name), encrypted);
|
|
474
|
+
const signature = await droneHandle.sign(dataToSign);
|
|
475
|
+
|
|
476
|
+
return { name, signature, encrypted };
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
β
**Benefits**:
|
|
481
|
+
- **Minimal registry**: Control center stores only device names (strings), not public keys
|
|
482
|
+
- **No key exchange**: Both parties derive the same key independently
|
|
483
|
+
- **Stateless verification**: Signature check proves device authenticity
|
|
484
|
+
- **Isolation**: Compromise of one device doesn't affect others (different Handles β different channel keys)
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
### 7. Stateless Multi-Device Synchronization
|
|
489
|
+
|
|
490
|
+
A user with the same Identity on multiple devices (phone, laptop, tablet) can derive identical Handles and secrets on each device without any synchronization protocol.
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
// On the phone:
|
|
494
|
+
const identity = await Identity.fromSeed(userSeed);
|
|
495
|
+
const messengerHandle = await identity.deriveHandle('messenger-main');
|
|
496
|
+
const handleId = messengerHandle.getId();
|
|
497
|
+
// Send handleId to server for registration
|
|
498
|
+
|
|
499
|
+
// On the laptop (later, no sync needed):
|
|
500
|
+
const identity = await Identity.fromSeed(userSeed); // Same seed
|
|
501
|
+
const messengerHandle = await identity.deriveHandle('messenger-main'); // Same name
|
|
502
|
+
const handleId = messengerHandle.getId(); // Identical handleId!
|
|
503
|
+
// Server recognizes the same user automatically
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
β
**Benefit**: Zero-knowledge multi-device support. No QR codes, no server-side key sync, no backup servers. The mathematics guarantees identity across devices.
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
### 8. Break-Glass Recovery and Inheritance
|
|
511
|
+
|
|
512
|
+
A user's entire digital identity can be recovered from a single seed phrase, even years later, on any device, without contacting any service provider.
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
// User stores seed phrase in a physical safe (or via Shamir's Secret Sharing with trusted heirs)
|
|
516
|
+
|
|
517
|
+
// Years later, on a new device:
|
|
518
|
+
const identity = await Identity.fromSeed(recoveredSeed);
|
|
519
|
+
|
|
520
|
+
// All handles are instantly recoverable:
|
|
521
|
+
const googleHandle = await identity.deriveHandle('google');
|
|
522
|
+
const bankHandle = await identity.deriveHandle('bank');
|
|
523
|
+
const messengerHandle = await identity.deriveHandle('messenger-main');
|
|
524
|
+
|
|
525
|
+
// All passwords are instantly recoverable:
|
|
526
|
+
const googlePassword = googleHandle.derivePassword('google');
|
|
527
|
+
const bankPassword = bankHandle.derivePassword('bank');
|
|
528
|
+
|
|
529
|
+
// All channel keys are instantly recoverable:
|
|
530
|
+
const messengerChannelKey = messengerHandle.deriveChannelKey('session-2026');
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
β
**Benefit**: True self-sovereignty. No company can lock you out of your identity. Recovery is a mathematical certainty, not a customer support ticket.
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
### 9. Ephemeral Delegated Access
|
|
538
|
+
|
|
539
|
+
Grant time-limited access to a contractor or temporary service by deriving a Handle with a time-bound name.
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
// Grant access to contractor until 2026-12-31
|
|
543
|
+
const contractorHandle = await identity.deriveHandle('contractor-acme-2026-12-31', {
|
|
544
|
+
displayName: 'ACME Corp Contractor',
|
|
545
|
+
role: 'auditor',
|
|
546
|
+
expiresAt: '2026-12-31T23:59:59Z'
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Share the Handle ID with the contractor's system
|
|
550
|
+
// Contractor uses this Handle for authenticated access
|
|
551
|
+
|
|
552
|
+
// After expiration:
|
|
553
|
+
// - Server rejects requests (checks expiresAt metadata)
|
|
554
|
+
// - User simply stops using this Handle
|
|
555
|
+
// - No cleanup needed β the Handle is just a name in the derivation tree
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
β
**Benefit**: Clean delegation without polluting the permanent identity. Expired Handles become inert cryptographic artifacts.
|
|
559
|
+
|
|
560
|
+
## π Cryptographic Details
|
|
561
|
+
|
|
562
|
+
### Key Derivation Algorithm
|
|
563
|
+
|
|
564
|
+
Handles are derived using **HKDF-SHA256** with domain separation:
|
|
565
|
+
|
|
566
|
+
```
|
|
567
|
+
HandlePrivateKey = HKDF-SHA256(
|
|
568
|
+
inputKeyMaterial = IdentityPrivateKey,
|
|
569
|
+
salt = empty,
|
|
570
|
+
info = "me2em/handle/v1/" + lowercase(name),
|
|
571
|
+
length = 32
|
|
572
|
+
)
|
|
573
|
+
HandlePublicKey = Ed25519.PublicKey(HandlePrivateKey)
|
|
574
|
+
HandleId = Base64Url(HandlePublicKey)
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Properties:**
|
|
578
|
+
- π **Deterministic**: Same inputs β same output across all implementations
|
|
579
|
+
- π **One-way**: Cannot derive Identity key from Handle key
|
|
580
|
+
- π§© **Isolated**: Each Handle uses independent Ed25519 keypair
|
|
581
|
+
|
|
582
|
+
### Channel Key Derivation
|
|
583
|
+
|
|
584
|
+
Channel keys are derived using **HKDF-SHA256** with the Handle's private key as input key material:
|
|
585
|
+
|
|
586
|
+
```
|
|
587
|
+
ChannelKey = HKDF-SHA256(
|
|
588
|
+
inputKeyMaterial = HandlePrivateKey,
|
|
589
|
+
salt = "me2em/channel/" + lowercase(context),
|
|
590
|
+
info = "me2em/channel/v1",
|
|
591
|
+
length = 32
|
|
592
|
+
)
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**Properties**:
|
|
596
|
+
- π **Deterministic**: Same Handle + same context β same 32-byte key
|
|
597
|
+
- π **Encapsulated**: Private key never leaves the Handle instance
|
|
598
|
+
- π§© **Domain-separated**: Different contexts produce different keys (e.g., `'telemetry-v1'` vs `'command-v1'`)
|
|
599
|
+
- β‘ **No key exchange**: Both parties derive the key independently, no ECDH needed
|
|
600
|
+
|
|
601
|
+
### Signature Scheme
|
|
602
|
+
|
|
603
|
+
- **Algorithm**: Ed25519 (RFC 8032)
|
|
604
|
+
- **Hash**: SHA-512 (via `@noble/ed25519`)
|
|
605
|
+
- **Encoding**: Raw bytes β base64url for transport
|
|
606
|
+
|
|
607
|
+
### Test Vectors
|
|
608
|
+
|
|
609
|
+
See [`specs/test-vectors.json`](../../specs/test-vectors.json) for canonical derivation examples to ensure cross-implementation compatibility.
|
|
610
|
+
|
|
611
|
+
Example vector:
|
|
612
|
+
```json
|
|
613
|
+
{
|
|
614
|
+
"seed_hex": "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a",
|
|
615
|
+
"handle_name": "work",
|
|
616
|
+
"expected_handle_id": "pK7xJ2mN8vQ3rL5wY9zB1cD4eF6gH8iJ0kL2mN4oP6qR8sT0uV2wX4yZ6aB8cD0",
|
|
617
|
+
"expected_public_key_hex": "a4b5c6d7e8f90123456789abcdef0123456789abcdef0123456789abcdef0123"
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## π Integration Guide
|
|
624
|
+
|
|
625
|
+
### Backend Verification (Node.js Example)
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
import { Handle } from '@me2em/core';
|
|
629
|
+
|
|
630
|
+
// Middleware to verify Handle-signed requests
|
|
631
|
+
export async function me2emAuthMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
632
|
+
const handleId = req.headers['x-handle-id'] as string;
|
|
633
|
+
const signature = req.headers['x-signature'] as string;
|
|
634
|
+
const payload = JSON.stringify(req.body);
|
|
635
|
+
|
|
636
|
+
if (!handleId || !signature) {
|
|
637
|
+
return res.status(401).json({ error: 'Missing authentication headers' });
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
// Decode base64url to Uint8Array
|
|
642
|
+
const publicKey = base64urlToBytes(handleId);
|
|
643
|
+
const sig = base64urlToBytes(signature);
|
|
644
|
+
const data = new TextEncoder().encode(payload);
|
|
645
|
+
|
|
646
|
+
// Verify signature
|
|
647
|
+
const isValid = await Handle.verify(sig, data, publicKey);
|
|
648
|
+
if (!isValid) throw new Error('Invalid signature');
|
|
649
|
+
|
|
650
|
+
// Attach handle info to request for downstream use
|
|
651
|
+
req.me2em = { handleId, publicKey };
|
|
652
|
+
next();
|
|
653
|
+
} catch (err) {
|
|
654
|
+
res.status(401).json({ error: 'Authentication failed' });
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function base64urlToBytes(str: string): Uint8Array {
|
|
659
|
+
const padding = '='.repeat((4 - (str.length % 4)) % 4);
|
|
660
|
+
const base64 = str.replace(/-/g, '+').replace(/_/g, '/') + padding;
|
|
661
|
+
return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Frontend Secure Key Handling
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
// Store encrypted seed in IndexedDB (never plaintext)
|
|
669
|
+
import { encryptWithPin } from './crypto/local';
|
|
670
|
+
|
|
671
|
+
async function saveIdentity(seed: string, pin: string) {
|
|
672
|
+
const encrypted = await encryptWithPin(seed, pin);
|
|
673
|
+
await db.identities.add({
|
|
674
|
+
id: 'current',
|
|
675
|
+
encryptedSeed: encrypted,
|
|
676
|
+
// seed is NEVER stored in plaintext
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Load and decrypt on login
|
|
681
|
+
async function loadIdentity(pin: string) {
|
|
682
|
+
const record = await db.identities.get('current');
|
|
683
|
+
if (!record) throw new Error('No identity found');
|
|
684
|
+
|
|
685
|
+
const seed = await decryptWithPin(record.encryptedSeed, pin);
|
|
686
|
+
return Identity.fromSeed(seed);
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
β
**Best practice**: Use Web Crypto API or secure enclave for PIN/biometric decryption.
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
## π‘οΈ Security Best Practices
|
|
695
|
+
|
|
696
|
+
### β
Do
|
|
697
|
+
|
|
698
|
+
- **Store seeds encrypted**: Use PIN/biometric + AES-GCM before persisting to IndexedDB
|
|
699
|
+
- **Clear memory**: Zero out seed/private key buffers after use (`buffer.fill(0)`)
|
|
700
|
+
- **Use short TTLs**: Rotate session tokens frequently (β€1 hour recommended)
|
|
701
|
+
- **Validate metadata server-side**: Never trust client-provided Handle metadata without verification
|
|
702
|
+
- **Pin dependencies**: Lock `@me2em/core` to specific version in `package.json`
|
|
703
|
+
|
|
704
|
+
### β Don't
|
|
705
|
+
|
|
706
|
+
- **Never transmit seeds**: The seed is the root of trust β keep it client-side only
|
|
707
|
+
- **Don't reuse Handles across contexts**: Derive separate Handles for different apps/services
|
|
708
|
+
- **Don't log private keys**: Ensure debugging output never includes key material
|
|
709
|
+
- **Don't disable signature verification**: Always verify Ed25519 signatures server-side
|
|
710
|
+
|
|
711
|
+
### Key Lifecycle
|
|
712
|
+
|
|
713
|
+
```
|
|
714
|
+
[User enters seed]
|
|
715
|
+
β
|
|
716
|
+
βΌ
|
|
717
|
+
[Derive Identity in RAM]
|
|
718
|
+
β
|
|
719
|
+
βΌ
|
|
720
|
+
[Derive Handle(s) as needed]
|
|
721
|
+
β
|
|
722
|
+
βΌ
|
|
723
|
+
[Sign challenge / data]
|
|
724
|
+
β
|
|
725
|
+
βΌ
|
|
726
|
+
[Zero out private key buffers] β Critical!
|
|
727
|
+
β
|
|
728
|
+
βΌ
|
|
729
|
+
[Keep only public HandleId for future use]
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## π§ͺ Testing
|
|
735
|
+
|
|
736
|
+
### Unit Tests
|
|
737
|
+
|
|
738
|
+
```bash
|
|
739
|
+
cd packages/protocol-core
|
|
740
|
+
pnpm test
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
Tests cover:
|
|
744
|
+
- β
Deterministic derivation (same seed + name β same HandleId)
|
|
745
|
+
- β
Signature generation and verification
|
|
746
|
+
- β
Edge cases (empty metadata, long names, unicode)
|
|
747
|
+
|
|
748
|
+
### Integration Test Example
|
|
749
|
+
|
|
750
|
+
```typescript
|
|
751
|
+
import { Identity } from '@me2em/core';
|
|
752
|
+
|
|
753
|
+
test('full auth flow', async () => {
|
|
754
|
+
// Client side
|
|
755
|
+
const identity = await Identity.fromSeed(testSeed);
|
|
756
|
+
const handle = await identity.deriveHandle('test');
|
|
757
|
+
const challenge = new TextEncoder().encode('nonce-123');
|
|
758
|
+
const signature = await handle.sign(challenge);
|
|
759
|
+
|
|
760
|
+
// Server side
|
|
761
|
+
const isValid = await Handle.verify(
|
|
762
|
+
signature,
|
|
763
|
+
challenge,
|
|
764
|
+
handle.getPublicKey()
|
|
765
|
+
);
|
|
766
|
+
expect(isValid).toBe(true);
|
|
767
|
+
expect(handle.getId()).toMatch(/^[A-Za-z0-9_-]{43}$/);
|
|
768
|
+
});
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
### Cross-Implementation Testing
|
|
772
|
+
|
|
773
|
+
Use [`specs/test-vectors.json`](../../specs/test-vectors.json) to verify your implementation matches the reference:
|
|
774
|
+
|
|
775
|
+
```typescript
|
|
776
|
+
import vectors from '@me2em/core/specs/test-vectors.json';
|
|
777
|
+
|
|
778
|
+
for (const vector of vectors) {
|
|
779
|
+
const identity = await Identity.fromSeed(vector.seed_hex);
|
|
780
|
+
const handle = await identity.deriveHandle(vector.handle_name);
|
|
781
|
+
|
|
782
|
+
expect(handle.getId()).toBe(vector.expected_handle_id);
|
|
783
|
+
expect(bytesToHex(handle.getPublicKey())).toBe(vector.expected_public_key_hex);
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
## π€ Contributing
|
|
790
|
+
|
|
791
|
+
We welcome contributions! See:
|
|
792
|
+
|
|
793
|
+
- π [CONTRIBUTING.md](../../CONTRIBUTING.md) β How to contribute code/docs
|
|
794
|
+
- π³οΈ [GOVERNANCE.md](../../GOVERNANCE.md) β Project decision-making process
|
|
795
|
+
- π [SECURITY.md](../../SECURITY.md) β Responsible disclosure policy
|
|
796
|
+
|
|
797
|
+
### Quick Start for Contributors
|
|
798
|
+
|
|
799
|
+
```bash
|
|
800
|
+
git clone https://github.com/me2em-org/me2em-protocol.git
|
|
801
|
+
cd me2em-protocol
|
|
802
|
+
pnpm install
|
|
803
|
+
|
|
804
|
+
# Run tests
|
|
805
|
+
pnpm -r test
|
|
806
|
+
|
|
807
|
+
# Build all packages
|
|
808
|
+
pnpm -r build
|
|
809
|
+
|
|
810
|
+
# Lint
|
|
811
|
+
pnpm -r lint
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
### Adding a New Feature
|
|
815
|
+
|
|
816
|
+
1. Open a [Discussion](https://github.com/me2em-org/me2em-protocol/discussions) to propose the change
|
|
817
|
+
2. Implement in a feature branch
|
|
818
|
+
3. Add tests covering new functionality
|
|
819
|
+
4. Update `specs/core.md` if protocol behavior changes
|
|
820
|
+
5. Submit PR with clear description and test results
|
|
821
|
+
|
|
822
|
+
---
|
|
823
|
+
|
|
824
|
+
## π¦ Package Info
|
|
825
|
+
|
|
826
|
+
```json
|
|
827
|
+
{
|
|
828
|
+
"name": "@me2em/core",
|
|
829
|
+
"version": "0.3.0-alpha.1",
|
|
830
|
+
"type": "module",
|
|
831
|
+
"main": "./dist/index.js",
|
|
832
|
+
"types": "./dist/index.d.ts",
|
|
833
|
+
"exports": {
|
|
834
|
+
".": {
|
|
835
|
+
"types": "./dist/index.d.ts",
|
|
836
|
+
"import": "./dist/index.js",
|
|
837
|
+
"require": "./dist/index.cjs"
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
"dependencies": {
|
|
841
|
+
"@noble/ed25519": "^3.1.0",
|
|
842
|
+
"@noble/hashes": "^2.2.0"
|
|
843
|
+
},
|
|
844
|
+
"engines": {
|
|
845
|
+
"node": ">=18.0.0"
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
---
|
|
851
|
+
|
|
852
|
+
## π Ecosystem
|
|
853
|
+
|
|
854
|
+
This package is part of the Me2em ecosystem:
|
|
855
|
+
|
|
856
|
+
@me2em/core β ΠΊΡΠΈΠΏΡΠΎΠ³ΡΠ°ΡΠΈΡ (Identity, Handle, derivePassword, deriveChannelKey)
|
|
857
|
+
@me2em/sdk β browser wrapper (IndexedDB, PIN, biometric, session management)
|
|
858
|
+
@me2em/react β UI ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΡ (SeedDisplay, SeedVerification, HandleManager)
|
|
859
|
+
@me2em/auth-middleware β NestJS/Express middleware Π΄Π»Ρ Handle.verify()
|
|
860
|
+
@me2em/server β (ΠΎΠΏΡΠΈΠΎΠ½Π°Π»ΡΠ½ΠΎ) Π³ΠΎΡΠΎΠ²ΡΠΉ NestJS backend ΠΈΠ· Phase 1
|
|
861
|
+
|
|
862
|
+
π Learn more: [github.com/me2em-org](https://github.com/me2em-org) | [docs.me2em.com](https://docs.me2em.com)
|
|
863
|
+
|
|
864
|
+
---
|
|
865
|
+
|
|
866
|
+
## π License
|
|
867
|
+
|
|
868
|
+
Apache License 2.0 β see [LICENSE](../../LICENSE) for details.
|
|
869
|
+
|
|
870
|
+
Β© 2026 Me2em Organization. Built for privacy, openness, and user sovereignty.
|