me2em Protocol API Reference - v0.4.4-alpha.2

๐Ÿ“ฆ @me2em/core โ€” Core Cryptographic Primitives

Core primitives for the Me2em authorization protocol: Identity, Handle, and secure channel derivation with Ed25519 cryptography.

npm version License Docs


@me2em/core provides the cryptographic foundation for the Me2em protocol โ€” a decentralized multi-context identity system. It enables:

  • Hierarchical Deterministic Identities: One seed โ†’ multiple isolated Handles
  • Zero-Knowledge Password Management: Deterministic password derivation without storage
  • Secure Channel Communication: Encrypted channels between Identity and Handles without key exchange
  • Stateless Authentication: Cryptographic proof without server-side session storage

npm install @me2em/core
# or
pnpm add @me2em/core
# or
yarn add @me2em/core

Dependencies:

  • @noble/ed25519 โ€” Ed25519 signatures
  • @noble/hashes โ€” HKDF, SHA-256

import { Identity, Handle } from '@me2em/core';

// 1. Create Identity from seed (32 bytes)
const seed = new Uint8Array(32).fill(42); // Replace with your secure seed
const identity = await Identity.fromSeed(seed);

// 2. Derive a contextual Handle
const workHandle = await identity.deriveHandle('work', {
displayName: 'Alice @ Work',
avatar: 'https://example.com/avatar.png'
});

// 3. Use Handle for cryptographic operations
const message = new TextEncoder().encode('Hello, world!');
const signature = await workHandle.sign(message);
const isValid = await Handle.verify(signature, message, workHandle.getPublicKey());

console.log('Handle ID:', workHandle.getId()); // base64url(publicKey)
console.log('Signature valid:', isValid); // true

For production applications, use BIP39 mnemonic phrases. @me2em/core provides built-in utilities to handle generation, validation, and conversion to the required 32-byte Ed25519 seed.

import { 
Identity,
generateSeedPhrase,
get32ByteSeedFromMnemonic,
validateSeedPhrase
} from '@me2em/core';

// 1. Generate a 12-word phrase (use 256 for 24 words)
const phrase = generateSeedPhrase(128);
console.log('Your seed:', phrase.join(' '));

// 2. (Optional) Validate a user-provided phrase
const validation = validateSeedPhrase(phrase);
if (!validation.isValid) throw new Error(validation.error);

// 3. Convert to 32-byte seed and create Identity
const seedBytes = await get32ByteSeedFromMnemonic(phrase);
const identity = await Identity.fromSeed(seedBytes);

Why is this better? BIP39 produces a 64-byte seed, but Ed25519 requires exactly 32 bytes. The get32ByteSeedFromMnemonic utility handles the SHA-256 hashing deterministically and safely, so you don't have to write boilerplate crypto code.


The root cryptographic identity, derived from a seed phrase.

class Identity {
// Create Identity from seed (32-byte Uint8Array)
static fromSeed(seed: Uint8Array): Promise<Identity>;

// Derive a new Handle with optional metadata
deriveHandle(name: string, metadata?: HandleMetadata): Promise<Handle>;

// Get root public key (for verification, never share private key)
getPublicKey(): Uint8Array;
}

Creates an Identity from a 32-byte seed.

const seed = new Uint8Array(32);
crypto.getRandomValues(seed); // Generate secure random seed
const identity = await Identity.fromSeed(seed);

Security: The seed must be kept secret. Never log, transmit, or store it in plaintext.

Derives a new Handle deterministically from the Identity.

const handle = await identity.deriveHandle('google', {
displayName: 'Alice Personal',
avatar: 'https://example.com/alice.png'
});

Parameters:

  • name: Unique identifier for this Handle (case-insensitive, trimmed)
  • metadata: Optional public data attached to the Handle

Returns: A Handle instance with its own keypair, derived deterministically from the Identity.

Key Properties:

  • ๐Ÿ” Deterministic: Same seed + same name โ†’ same Handle (always)
  • ๐Ÿ”— Isolated: Each Handle has its own keypair; compromise of one does not affect others
  • ๐Ÿ”’ One-way: Cannot derive Identity key from Handle key

Built-in helpers for human-friendly key generation and validation.

// Generate a new phrase (128 bits = 12 words, 256 bits = 24 words)
function generateSeedPhrase(strength: 128 | 256 = 128): string[];

// Normalize input (handles extra spaces, lowercase)
function normalizeSeedPhrase(input: string | string[]): string[];

// Validate word count, wordlist, and BIP39 checksum
function validateSeedPhrase(words: string[]): { isValid: boolean; error?: string };

// Convert validated mnemonic to a secure 32-byte Uint8Array for Ed25519
async function get32ByteSeedFromMnemonic(phrase: string | string[]): Promise<Uint8Array>;

โœ… Benefit: Developers don't need to manually manage @scure/bip39 imports or remember to hash the 64-byte output. Everything is handled securely within the protocol.


A derived Ed25519 keypair representing a specific context (work, personal, IoT device).

class Handle {
// Get public identifier (safe to share)
getId(): string; // base64url-encoded public key

// Sign arbitrary data with Handle's private key
sign(data: Uint8Array): Promise<Uint8Array>;

// Verify a signature using a public key (static method)
static verify(signature: Uint8Array, data: Uint8Array, publicKey: Uint8Array): Promise<boolean>;

// Deterministically derive a password/secret for a specific context
derivePassword(context: string, length?: number): string;

// Derive a symmetric channel key for encrypted communication
deriveChannelKey(context: string): Uint8Array;

// Accessors
getName(): string;
getMetadata(): HandleMetadata | undefined;
getPublicKey(): Uint8Array;
}

Returns a URL-safe base64 string representing the Handle's public key:

const handleId = handle.getId();
// Example: "pK7xJ2mN8vQ3rL5wY9zB1cD4eF6gH8iJ0kL2mN4oP6qR8sT0uV2wX4yZ6aB8cD0"

This is the identifier you send to servers for authentication and routing.

Signs arbitrary binary data using Ed25519:

const payload = new TextEncoder().encode('{"action":"post","content":"Hello"}');
const signature = await handle.sign(payload);

// Send to server:
fetch('https://api.example.com/endpoint', {
method: 'POST',
headers: {
'X-Handle-ID': handle.getId(),
'Content-Type': 'application/json'
},
body: JSON.stringify({
payload: btoa(String.fromCharCode(...payload)),
signature: btoa(String.fromCharCode(...signature))
})
});

Static method for server-side signature verification:

// Server receives: handleId, signature, payload
const publicKey = Uint8Array.from(atob(handleId), c => c.charCodeAt(0));
const data = Uint8Array.from(atob(payload), c => c.charCodeAt(0));
const sig = Uint8Array.from(atob(signature), c => c.charCodeAt(0));

const isValid = await Handle.verify(sig, data, publicKey);
if (!isValid) throw new Error('Invalid signature');

Deterministically derives a secret (e.g., a password or API key) for a specific service context. The private key never leaves this class, ensuring maximum security.

// Derive a password for a specific service
const googlePassword = workHandle.derivePassword('google');
// Example output: "xK9mP2qL5wY9zB1cD4eF6g"

// Derive a longer secret (e.g., 32 bytes for an API key)
const apiKey = workHandle.derivePassword('aws-api', 32);

Parameters:

  • context: A unique identifier for the service (e.g., 'google', 'github', 'wifi-router')
  • length: Length of the derived raw bytes (default: 16 bytes = ~22 chars base64url)

Returns: A URL-safe base64 string suitable for use as a strong password.

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.

Use Case: 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.

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.

// On the device (Handle side):
const channelKey = droneHandle.deriveChannelKey('telemetry-v1');
// channelKey is a 32-byte Uint8Array, ready for AES-256-GCM

// On the controller (Identity side):
const droneHandle = await centerIdentity.deriveHandle('drone-001');
const channelKey = droneHandle.deriveChannelKey('telemetry-v1');
// Identical key, derived independently

Parameters:

  • context: Channel identifier for domain separation (e.g., 'drone-001', 'session-abc'). Both parties must use the same context to derive the same key.

Returns: 32-byte Uint8Array suitable for AES-256-GCM encryption.

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.

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.


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.

// 1. User restores Identity from Seed (e.g., after entering a PIN)
const identity = await Identity.fromSeed(userSeed);

// 2. Derive the specific service Handle
const googleHandle = await identity.deriveHandle('google', {
displayName: 'Alice Personal'
});

// 3. Deterministically generate the password on the fly
const password = googleHandle.derivePassword('google');

// 4. Auto-fill the login form
console.log('Login:', 'alice@example.com');
console.log('Password:', password); // Always the same for this seed + handle + context

โœ… 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-v2')).


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.

// === CONTROL CENTER (Identity) ===
const centerIdentity = await Identity.fromSeed(centerSeed);

// Minimal registry: just device names (no public keys stored!)
const allowedDevices = ['drone-001', 'drone-002', 'sensor-warehouse-a'];

// Receiving telemetry from a drone
async function receiveTelemetry(message: { name: string, signature: Uint8Array, encrypted: Uint8Array }) {
// 1. Check if device is in registry
if (!allowedDevices.includes(message.name)) {
throw new Error('Unknown device');
}

// 2. Derive the Handle (deterministic, no DB lookup)
const deviceHandle = await centerIdentity.deriveHandle(message.name);

// 3. Verify signature (proves device owns the private key)
const dataToVerify = concatBytes(
new TextEncoder().encode(message.name),
message.encrypted
);
const isValid = await Handle.verify(message.signature, dataToVerify, deviceHandle.getPublicKey());
if (!isValid) throw new Error('Invalid signature');

// 4. Derive the SAME channel key the device used for encryption
const channelKey = deviceHandle.deriveChannelKey('telemetry-v1');

// 5. Decrypt the message
const telemetry = await decryptAESGCM(message.encrypted, channelKey);
return telemetry;
}

// === DRONE (Handle, provisioned at factory) ===
// Drone is provisioned with its Handle's private key and name
async function sendTelemetry(telemetry: object) {
const name = 'drone-001';
const privateKey = /* loaded from secure enclave */;
const droneHandle = new Handle(privateKey, name);

// 1. Derive the SAME channel key the center will use for decryption
const channelKey = droneHandle.deriveChannelKey('telemetry-v1');

// 2. Encrypt telemetry
const telemetryBytes = new TextEncoder().encode(JSON.stringify(telemetry));
const encrypted = await encryptAESGCM(telemetryBytes, channelKey);

// 3. Sign (name + encrypted data)
const dataToSign = concatBytes(new TextEncoder().encode(name), encrypted);
const signature = await droneHandle.sign(dataToSign);

return { name, signature, encrypted };
}

โœ… Benefits:

  • Minimal registry: Control center stores only device names (strings), not public keys
  • No key exchange: Both parties derive the same key independently
  • Stateless verification: Signature check proves device authenticity
  • Isolation: Compromise of one device doesn't affect others (different Handles โ†’ different channel keys)

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.

// On the phone:
const identity = await Identity.fromSeed(userSeed);
const messengerHandle = await identity.deriveHandle('messenger-main');
const handleId = messengerHandle.getId();
// Send handleId to server for registration

// On the laptop (later, no sync needed):
const identity = await Identity.fromSeed(userSeed); // Same seed
const messengerHandle = await identity.deriveHandle('messenger-main'); // Same name
const handleId = messengerHandle.getId(); // Identical handleId!
// Server recognizes the same user automatically

โœ… Benefit: Zero-knowledge multi-device support. No QR codes, no server-side key sync, no backup servers. The mathematics guarantees identity across devices.


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.

// User stores seed phrase in a physical safe (or via Shamir's Secret Sharing with trusted heirs)

// Years later, on a new device:
const identity = await Identity.fromSeed(recoveredSeed);

// All handles are instantly recoverable:
const googleHandle = await identity.deriveHandle('google');
const bankHandle = await identity.deriveHandle('bank');
const messengerHandle = await identity.deriveHandle('messenger-main');

// All passwords are instantly recoverable:
const googlePassword = googleHandle.derivePassword('google');
const bankPassword = bankHandle.derivePassword('bank');

// All channel keys are instantly recoverable:
const messengerChannelKey = messengerHandle.deriveChannelKey('session-2026');

โœ… Benefit: True self-sovereignty. No company can lock you out of your identity. Recovery is a mathematical certainty, not a customer support ticket.


Grant time-limited access to a contractor or temporary service by deriving a Handle with a time-bound name.

// Grant access to contractor until 2026-12-31
const contractorHandle = await identity.deriveHandle('contractor-acme-2026-12-31', {
displayName: 'ACME Corp Contractor',
role: 'auditor',
expiresAt: '2026-12-31T23:59:59Z'
});

// Share the Handle ID with the contractor's system
// Contractor uses this Handle for authenticated access

// After expiration:
// - Server rejects requests (checks expiresAt metadata)
// - User simply stops using this Handle
// - No cleanup needed โ€” the Handle is just a name in the derivation tree

โœ… Benefit: Clean delegation without polluting the permanent identity. Expired Handles become inert cryptographic artifacts.


Handles are derived using HKDF-SHA256 with domain separation:

HandlePrivateKey = HKDF-SHA256(
inputKeyMaterial = IdentityPrivateKey,
salt = empty,
info = "me2em/handle/v1/" + lowercase(name),
length = 32
)
HandlePublicKey = Ed25519.PublicKey(HandlePrivateKey)
HandleId = Base64Url(HandlePublicKey)

Properties:

  • ๐Ÿ” Deterministic: Same inputs โ†’ same output across all implementations
  • ๐Ÿ”’ One-way: Cannot derive Identity key from Handle key
  • ๐Ÿงฉ Isolated: Each Handle uses independent Ed25519 keypair

Passwords are derived using HKDF-SHA256 with the Handle's private key as input key material:

PasswordBytes = HKDF-SHA256(
inputKeyMaterial = HandlePrivateKey,
salt = "me2em/secret/" + lowercase(context),
info = "me2em/secret/v1",
length = 16 (default)
)
Password = Base64Url(PasswordBytes)

Properties:

  • ๐Ÿ” Deterministic: Same Handle + same context โ†’ same password
  • ๐Ÿ” Encapsulated: Private key never leaves the Handle instance
  • ๐Ÿงฉ Domain-separated: Different contexts produce different passwords

Channel keys are derived using HKDF-SHA256 with the Handle's private key as input key material:

ChannelKey = HKDF-SHA256(
inputKeyMaterial = HandlePrivateKey,
salt = "me2em/channel/" + lowercase(context),
info = "me2em/channel/v1",
length = 32
)

Properties:

  • ๐Ÿ” Deterministic: Same Handle + same context โ†’ same 32-byte key
  • ๐Ÿ” Encapsulated: Private key never leaves the Handle instance
  • ๐Ÿงฉ Domain-separated: Different contexts produce different keys (e.g., 'telemetry-v1' vs 'command-v1')
  • โšก No key exchange: Both parties derive the key independently, no ECDH needed
  • Algorithm: Ed25519 (RFC 8032)
  • Hash: SHA-512 (via @noble/ed25519)
  • Encoding: Raw bytes โ†’ base64url for transport

  • Store seeds encrypted: Use PIN/biometric + AES-GCM before persisting to IndexedDB
  • Clear memory: Zero out seed/private key buffers after use (buffer.fill(0))
  • Use short TTLs: Rotate session tokens frequently (โ‰ค1 hour recommended)
  • Validate metadata server-side: Never trust client-provided Handle metadata without verification
  • Pin dependencies: Lock @me2em/core to specific version in package.json
  • Never transmit seeds: The seed is the root of trust โ€” keep it client-side only
  • Don't reuse Handles across contexts: Derive separate Handles for different apps/services
  • Don't log private keys: Ensure debugging output never includes key material
  • Don't disable signature verification: Always verify Ed25519 signatures server-side
[User enters seed]
โ”‚
โ–ผ
[Derive Identity in RAM]
โ”‚
โ–ผ
[Derive Handle(s) as needed]
โ”‚
โ–ผ
[Sign challenge / data / derive password / derive channel key]
โ”‚
โ–ผ
[Zero out private key buffers] โ† Critical!
โ”‚
โ–ผ
[Keep only public HandleId for future use]

cd packages/core
pnpm test

Tests cover:

  • โœ… Deterministic derivation (same seed + name โ†’ same HandleId)
  • โœ… Signature generation and verification
  • โœ… Password derivation (same Handle + context โ†’ same password)
  • โœ… Channel key derivation (same Handle + context โ†’ same key)
  • โœ… Edge cases (empty metadata, long names, unicode)
import { Identity } from '@me2em/core';

test('full auth flow', async () => {
// Client side
const identity = await Identity.fromSeed(testSeed);
const handle = await identity.deriveHandle('test');
const challenge = new TextEncoder().encode('nonce-123');
const signature = await handle.sign(challenge);

// Server side
const isValid = await Handle.verify(
signature,
challenge,
handle.getPublicKey()
);
expect(isValid).toBe(true);
expect(handle.getId()).toMatch(/^[A-Za-z0-9_-]{43}$/);
});

{
"name": "@me2em/core",
"version": "0.4.2-alpha.1",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"dependencies": {
"@noble/ed25519": "^3.1.0",
"@noble/hashes": "^2.2.0",
"@scure/bip39": "^1.3.0"
},
"engines": {
"node": ">=18.0.0"
}
}

We welcome contributions! See:

  • ๐Ÿ“„ CONTRIBUTING.md โ€” How to contribute code/docs
  • ๐Ÿ—ณ๏ธ GOVERNANCE.md โ€” Project decision-making process
  • ๐Ÿ” SECURITY.md โ€” Responsible disclosure policy

Apache License 2.0 โ€” see LICENSE for details.

ยฉ 2026 Me2em Organization. Built for privacy, openness, and user sovereignty.