@cryptforge/auth
v0.2.1
Published
Browser-compatible authentication and key management for cryptocurrency wallets
Maintainers
Readme
@cryptforge/auth
Browser-compatible authentication and key management for cryptocurrency wallets. Built on industry-standard BIP39/BIP44 with secure keystore encryption.
Features
- 🔐 Secure Key Management - BIP39 mnemonics encrypted with PBKDF2 + AES-256-CBC
- 🌐 Browser-First - Zero configuration, works everywhere (browser, Node.js, Electron, React Native)
- 🔗 Multi-Blockchain - Pluggable adapter system for any blockchain
- 👤 Multi-Identity - Manage multiple wallets with separate keystores
- 🔒 Auto-Lock - Configurable session timeouts for security
- ⏱️ Key Expiration - Automatic tracking with live countdowns
- 💾 IndexedDB Storage - Encrypted keystores persisted locally
- 🎯 Type-Safe - Full TypeScript support
- 📦 Lightweight - ~26 KB minified
Installation
npm install @cryptforge/auth @cryptforge/blockchain-evm @cryptforge/blockchain-btcQuick Start
import { createAuthClient } from "@cryptforge/auth";
import { EVMAdapter } from "@cryptforge/blockchain-evm";
import { BitcoinAdapter } from "@cryptforge/blockchain-btc";
// Create auth client
const auth = createAuthClient();
// Register blockchain adapters
auth.registerAdapter(
"ethereum",
new EVMAdapter({
chainData: { name: "Ethereum", symbol: "ETH", cmc_id: 1027 },
coinType: 60,
})
);
auth.registerAdapter("bitcoin", new BitcoinAdapter());
// Generate mnemonic
const mnemonic = auth.generateMnemonic({ wordCount: 12 });
// Create identity
const { identity, keys } = await auth.createIdentity({
mnemonic,
password: "secure-password",
label: "Personal Wallet",
chainId: "ethereum",
});
console.log("Address:", keys.address);Usage Guide
First Time Setup
1. Generate and Backup Mnemonic
// Generate a new mnemonic
const mnemonic = auth.generateMnemonic({ wordCount: 12 });
// Example: "abandon ability able about above absent absorb abstract absurd abuse access accident"
// ⚠️ CRITICAL: Display to user and require backup confirmation
// User must write down or securely store the mnemonic2. Create Identity
const { identity, keys } = await auth.createIdentity({
mnemonic: mnemonic,
password: "user-chosen-password",
label: "Personal Wallet",
metadata: {
createdBy: "MyApp",
version: "1.0.0",
},
chainId: "ethereum", // Optional: unlock with specific chain
});
// Identity created and encrypted in IndexedDB
// If chainId provided, wallet is unlocked and ready to useReturning User Flow
1. List Available Identities
const identities = await auth.listIdentities();
// [
// {
// id: 'identity_A1B2C3D4',
// label: 'Personal Wallet',
// fingerprint: 'A1B2C3D4',
// createdAt: Date,
// lastAccess: Date
// }
// ]2. Select and Unlock
// Select an identity
await auth.switchIdentity(identities[0].id);
// Unlock with password
const { keys } = await auth.unlock({
password: "user-chosen-password",
chainId: "ethereum",
duration: 10 * 60 * 1000, // Auto-lock after 10 minutes
});
// Wallet is now unlocked and ready to sign
console.log("Address:", keys.address);
console.log("Expires in:", auth.currentExpiresIn, "seconds");Core Features
Identity Management
Create Identity
const { identity, keys } = await auth.createIdentity({
mnemonic: "word1 word2 ... word12",
password: "secure-password",
label: "Trading Wallet",
metadata: { purpose: "trading" },
chainId: "ethereum", // Optional
});Import Identity
// From mnemonic backup
const { identity } = await auth.importIdentity(
"word1 word2 ... word12",
"new-password",
"mnemonic"
);
// From keystore JSON
const { identity } = await auth.importIdentity(
keystoreJsonString,
"password",
"keystore"
);Export Identity
// Export as mnemonic (for backup)
const { data: mnemonic } = await auth.exportIdentity(identity.id, {
password: "password",
format: "mnemonic",
});
// Export as keystore JSON
const { data: keystore } = await auth.exportIdentity(identity.id, {
password: "password",
format: "keystore",
});Delete Identity
// Permanently delete (requires password)
await auth.deleteIdentity(identity.id, "password");
// ⚠️ Make sure mnemonic is backed up first!Update Identity
await auth.updateIdentity(identity.id, {
label: "Updated Wallet Name",
metadata: { theme: "dark" },
});Change Password
await auth.changePassword(identity.id, "old-password", "new-password");Session Management
Unlock
const { keys } = await auth.unlock({
password: "password",
chainId: "ethereum",
duration: 15 * 60 * 1000, // Optional: auto-lock after 15 min
});
// Access derived keys
console.log("Address:", keys.address);
console.log("Public Key:", keys.publicKeyHex);
console.log("Derivation Path:", keys.derivationPath);
console.log("Expires At:", keys.expiresAt);Lock
await auth.lock();
// Clears keys from memory
// Keystore remains encrypted in IndexedDBBlockchain Operations
Switch Chain
// Switch to different blockchain (if already unlocked)
await auth.switchChain("bitcoin");
// Switch when locked (requires password)
await auth.switchChain("bitcoin", "password");Get Addresses
// Get multiple addresses for a chain
const addresses = await auth.getAddresses("ethereum", 0, 5);
// [
// { address: '0x...', path: "m/44'/60'/0'/0/0", index: 0 },
// { address: '0x...', path: "m/44'/60'/0'/0/1", index: 1 },
// ...
// ]
// Get specific address by index
const { address, publicKey, derivationPath } = await auth.getAddressForChain(
"bitcoin",
0
);Find Used Addresses (Account Discovery)
// Find all addresses with balance
const usedAddresses = await auth.findUsedAddresses(
"ethereum",
async (address) => {
// Your balance checking logic
const balance = await checkBalance(address);
return balance > 0;
}
);
// Scans with BIP44 gap limit of 20Cryptographic Operations
Sign Message
const { signature, address, publicKey } = await auth.signMessage({
message: "Hello CryptForge!",
});
// Sign with different derivation path
const result = await auth.signMessage({
message: "Custom path",
derivationPath: "m/44'/60'/0'/0/1",
});Sign Transaction
const { signedTransaction, signature } = await auth.signTransaction({
transaction: {
to: "0x...",
value: "1000000000000000000", // 1 ETH in wei
gasLimit: 21000,
},
});
// Sign with different key
const result = await auth.signTransaction({
transaction: tx,
derivationPath: "m/44'/60'/0'/0/5",
});Verify Signature
const isValid = await auth.verifySignature(
"Hello CryptForge!",
signature,
publicKey
);Key Management
Rotate Keys
// Rotate to next address index
const { keys } = await auth.rotateKeys();
console.log("New address:", keys.address);
// Rotate to custom derivation path
const { keys } = await auth.rotateKeys("m/44'/60'/0'/0/5");Derive Custom Key
// One-time key derivation (not stored in session)
const { privateKey, publicKey, address, path } = await auth.deriveKey({
path: "m/44'/60'/1'/0/0", // Different account
});State Management
Getters
// Identity state
auth.currentIdentity; // Identity | null
auth.hasIdentity; // boolean
// Keys state
auth.currentKeys; // Keys | null
auth.currentAddress; // string | null
auth.currentPublicKey; // string | null (hex)
// Chain state
auth.currentChain; // Chain | null
// Lock state
auth.isLocked; // boolean
auth.isUnlocked; // boolean
// Expiration state
auth.currentExpiresAt; // Date | null
auth.currentExpiresIn; // number | null (seconds remaining)
// Available chains
auth.getRegisteredChains(); // string[]Event Subscription
const unsubscribe = auth.onAuthStateChange((event, keys) => {
console.log("Event:", event);
switch (event) {
case "IDENTITY_CREATED":
console.log("New identity created");
break;
case "UNLOCKED":
console.log("Wallet unlocked:", keys?.address);
break;
case "LOCKED":
console.log("Wallet locked");
break;
case "CHAIN_SWITCHED":
console.log("Switched to:", keys?.chain.name);
break;
case "KEYS_ROTATED":
console.log("Keys rotated to:", keys?.address);
break;
case "KEYS_EXPIRED":
console.log("Keys expired, please unlock again");
break;
// ... other events
}
});
// Unsubscribe when done
unsubscribe();Available Events
IDENTITY_CREATED- New identity createdIDENTITY_RESTORED- Identity imported/restoredIDENTITY_SWITCHED- Switched to different identityIDENTITY_UPDATED- Identity metadata updatedIDENTITY_DELETED- Identity removedPASSWORD_CHANGED- Password updatedUNLOCKED- Wallet unlocked with keysLOCKED- Wallet lockedKEYS_ROTATED- Keys rotated to new addressKEYS_EXPIRED- Keys passed expiration timeKEY_DERIVED- Custom key derivedCHAIN_SWITCHED- Switched to different blockchain
Blockchain Adapters
Registering Adapters
import { EVMAdapter } from "@cryptforge/blockchain-evm";
import { BitcoinAdapter } from "@cryptforge/blockchain-btc";
// Ethereum
auth.registerAdapter(
"ethereum",
new EVMAdapter({
chainData: { name: "Ethereum", symbol: "ETH", cmc_id: 1027 },
coinType: 60,
networks: {
mainnet: {
name: "Ethereum Mainnet",
rpcUrl: "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY",
chainId: 1,
},
},
})
);
// Bitcoin
auth.registerAdapter("bitcoin", new BitcoinAdapter());
// Polygon (EVM-compatible)
auth.registerAdapter(
"polygon",
new EVMAdapter({
chainData: { name: "Polygon", symbol: "MATIC", cmc_id: 3890 },
coinType: 60,
networks: {
mainnet: {
name: "Polygon Mainnet",
rpcUrl: "https://polygon-rpc.com",
chainId: 137,
},
},
})
);Creating Custom Adapters
Implement the BlockchainAdapter interface to add support for new blockchains:
import type { BlockchainAdapter, KeyData, ChainData } from "@cryptforge/core";
class SolanaAdapter implements BlockchainAdapter {
readonly chainData: ChainData = {
name: "Solana",
symbol: "SOL",
cmc_id: 5426,
};
async deriveKeys(mnemonic: string): Promise<KeyData> {
// Implement Solana key derivation
}
async deriveKeysAtIndex(mnemonic: string, index: number): Promise<KeyData> {
// Implement indexed derivation
}
// ... implement all required methods
}
// Register your custom adapter
auth.registerAdapter("solana", new SolanaAdapter());API Reference
Methods
Identity Management
| Method | Parameters | Returns | Description |
| ------------------ | ------------------------------------------------------------------- | ----------------------------- | ----------------------- |
| generateMnemonic | options?: { wordCount?: 12 \| 24 } | string | Generate BIP39 mnemonic |
| createIdentity | CreateIdentityOptions & { chainId?: string } | Promise<{ identity, keys }> | Create new identity |
| importIdentity | data: string, password: string, format?: 'mnemonic' \| 'keystore' | Promise<{ identity }> | Import from backup |
| exportIdentity | identityId: string, options: ExportOptions | Promise<{ format, data }> | Export identity |
| listIdentities | - | Promise<Identity[]> | Get all identities |
| switchIdentity | identityId: string | Promise<{ identity }> | Switch active identity |
| updateIdentity | identityId: string, updates: { label?, metadata? } | Promise<{ identity }> | Update metadata |
| deleteIdentity | identityId: string, password: string | Promise<void> | Delete identity |
| changePassword | identityId: string, oldPassword: string, newPassword: string | Promise<void> | Change password |
Session Management
| Method | Parameters | Returns | Description |
| -------- | --------------- | ------------------- | ---------------------- |
| unlock | UnlockOptions | Promise<{ keys }> | Decrypt and load keys |
| lock | - | Promise<void> | Clear keys from memory |
Chain Management
| Method | Parameters | Returns | Description |
| --------------------- | --------------------------------------------- | ------------------- | ------------------------ |
| registerAdapter | chainId: string, adapter: BlockchainAdapter | void | Register blockchain |
| switchChain | chainId: string, password?: string | Promise<{ keys }> | Switch blockchain |
| getRegisteredChains | - | string[] | Get registered chain IDs |
Key Operations
| Method | Parameters | Returns | Description |
| ------------ | ---------------------------- | --------------------------------------------------- | ---------------------- |
| rotateKeys | newDerivationPath?: string | Promise<{ keys }> | Rotate to next address |
| deriveKey | DeriveKeyOptions | Promise<{ privateKey, publicKey, address, path }> | One-time derivation |
Cryptographic Operations
| Method | Parameters | Returns | Description |
| ----------------- | ------------------------------- | -------------------------------------------- | ---------------- |
| signMessage | SignMessageOptions | Promise<{ signature, address, publicKey }> | Sign message |
| signTransaction | SignTransactionOptions | Promise<{ signedTransaction, signature }> | Sign transaction |
| verifySignature | message, signature, publicKey | Promise<boolean> | Verify signature |
Address Management
| Method | Parameters | Returns | Description |
| -------------------- | ------------------------------------------------- | ------------------------------------------------- | ---------------------- |
| getAddressForChain | chainId: string, index?: number | Promise<{ address, publicKey, derivationPath }> | Get single address |
| getAddresses | chainId: string, start?: number, count?: number | Promise<Array<{ address, path, index }>> | Get multiple addresses |
| findUsedAddresses | chainId: string, checkBalance: Function | Promise<Array<{ address, path, index }>> | Account discovery |
Event Management
| Method | Parameters | Returns | Description |
| ------------------- | ------------------------------ | ------------ | ----------------------------------------- |
| onAuthStateChange | callback: AuthChangeCallback | () => void | Subscribe to events (returns unsubscribe) |
Types
CreateIdentityOptions
interface CreateIdentityOptions {
mnemonic: string; // BIP39 mnemonic (12 or 24 words)
password: string; // Password to encrypt keystore
label?: string; // Human-readable label
metadata?: Record<string, any>; // Custom metadata
chainId?: string; // Optional: unlock with specific chain
}UnlockOptions
interface UnlockOptions {
password: string; // Keystore decryption password
identityId?: string; // Which identity to unlock (defaults to current)
chainId?: string; // Which blockchain to use (required)
derivationPath?: string; // Custom derivation path (advanced)
duration?: number; // Auto-lock duration in ms
}Identity
interface Identity {
id: string; // Unique identifier
publicKey: string; // Master public key (xpub)
fingerprint: string; // Master key fingerprint
label?: string; // User-defined label
metadata: Record<string, any>;
createdAt: Date;
lastAccess?: Date;
}Keys
interface Keys {
privateKey: Uint8Array; // Derived private key (binary)
privateKeyHex: string; // Derived private key (hex)
publicKey: Uint8Array; // Derived public key (binary)
publicKeyHex: string; // Derived public key (hex)
address: string; // Blockchain address
derivationPath: string; // BIP44 path (e.g., "m/44'/60'/0'/0/0")
chain: Chain; // Current blockchain
expiresAt: Date; // When keys expire
expiresIn: number; // Seconds until expiration (at creation)
identity: Identity; // Associated identity
}Chain
interface Chain {
id: string; // Chain identifier (e.g., 'ethereum')
name: string; // Display name (e.g., 'Ethereum')
symbol: string; // Token symbol (e.g., 'ETH')
}Security Best Practices
1. Mnemonic Backup
// Always require user confirmation before creating identity
const mnemonic = auth.generateMnemonic({ wordCount: 12 });
// Show to user with clear warnings:
// ⚠️ Write down these words in order
// ⚠️ Never share with anyone
// ⚠️ Store in a secure location
// ⚠️ Losing these words = losing access to funds
// Only proceed after confirmation
await auth.createIdentity({ mnemonic, password, ... });2. Password Requirements
// Enforce strong passwords
function isStrongPassword(password: string): boolean {
return (
password.length >= 12 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password) &&
/[^A-Za-z0-9]/.test(password)
);
}3. Auto-Lock
// Always use auto-lock for security
await auth.unlock({
password,
chainId: "ethereum",
duration: 5 * 60 * 1000, // Lock after 5 minutes of inactivity
});4. Key Expiration Monitoring
// Monitor expiration in your UI
setInterval(() => {
const remaining = auth.currentExpiresIn;
if (remaining !== null && remaining < 60) {
console.warn("Keys expiring soon!");
// Show UI warning
}
}, 1000);
// Listen for expiration event
auth.onAuthStateChange((event) => {
if (event === "KEYS_EXPIRED") {
// Prompt user to unlock again
}
});5. Secure Key Handling
// Never log private keys in production
if (process.env.NODE_ENV !== "production") {
console.log("Private Key:", keys.privateKeyHex);
}
// Use public key for verification
console.log("Public Key:", keys.publicKeyHex); // Safe to log
// Lock when not in use
window.addEventListener("beforeunload", () => {
auth.lock();
});Advanced Usage
Multiple Identities
// Create multiple wallets
const personal = await auth.createIdentity({
mnemonic: generateMnemonic(),
password: "password1",
label: "Personal",
});
const trading = await auth.createIdentity({
mnemonic: generateMnemonic(),
password: "password2",
label: "Trading",
});
// Switch between them
await auth.switchIdentity(trading.identity.id);
await auth.unlock({ password: "password2", chainId: "ethereum" });Custom Derivation Paths
// Derive keys at custom path
const customKey = await auth.deriveKey({
path: "m/44'/60'/1'/0/0", // Account 1 instead of 0
});
// Sign with custom path
const { signature } = await auth.signMessage({
message: "Hello",
derivationPath: "m/44'/60'/1'/0/0",
});React/Vue Integration
import { ref, onMounted, onUnmounted } from "vue";
const currentAddress = ref(auth.currentAddress);
const expiresIn = ref(auth.currentExpiresIn);
// Subscribe to changes
const unsubscribe = auth.onAuthStateChange((event, keys) => {
currentAddress.value = auth.currentAddress;
expiresIn.value = auth.currentExpiresIn;
});
// Live expiration countdown
const interval = setInterval(() => {
expiresIn.value = auth.currentExpiresIn;
}, 1000);
// Cleanup
onUnmounted(() => {
unsubscribe();
clearInterval(interval);
});Master Public Key
Get the master public key derived from the mnemonic.
Security Note
This returns ONLY the public key bytes (33 bytes, compressed secp256k1), NOT the extended public key (xpub). This is safe to expose publicly because:
- ✅ Cannot derive child keys (no chaincode included)
- ✅ Cannot compute private keys
- ✅ Standard public key cryptography - public keys are meant to be public
- ✅ Chain-independent - same key regardless of blockchain
Use Cases
- Document ownership verification
- Identity verification across different blockchains
- Signature verification
- Access control (e.g., "owner" field in documents)
What This Is NOT
- ❌ NOT an extended public key (xpub) - does not include chaincode
- ❌ NOT a blockchain address - this is the raw master public key
- ❌ NOT blockchain-specific - same for all chains
Usage
await auth.unlock({ password: 'my-password' });
const masterPubKey = auth.masterPublicKey;
// "02a1b2c3d4e5f6..." (33 bytes hex, compressed secp256k1)
// Use for document ownership
const document = {
id: 'doc_123',
owner: masterPubKey, // ← Chain-independent identity!
data: { ... }
};
// Sign with current blockchain key
const signature = await auth.signMessage({
message: `${document.id}:update:${Date.now()}`
});
// Server can verify ownership regardless of chainBrowser Compatibility
This package is 100% browser-compatible with zero configuration:
- ✅ Chrome, Firefox, Safari, Edge
- ✅ Node.js (v16+)
- ✅ Electron (main and renderer)
- ✅ React Native
- ✅ Web Workers
- ✅ Service Workers
No polyfills required. Uses native browser APIs:
crypto.subtlefor encryptionindexedDBfor storageUint8Arrayfor binary data
Dependencies
{
"@scure/bip39": "^1.2.1",
"@scure/bip32": "^1.3.2",
"@cryptforge/core": "workspace:*"
}All dependencies are browser-safe, audited, and actively maintained.
Additional Documentation
- DATA_ENCRYPTION.md - Detailed documentation on HKDF data encryption and master public key features for chain-independent encryption and identity verification
Examples
See the complete working example in examples/vue-electron-example/src/AuthTest.vue.
License
MIT
Contributing
Contributions welcome! Please ensure:
- All code is browser-compatible (use
@scureand@noblelibraries) - TypeScript types are properly defined
- Examples are updated
- Tests pass (when implemented)
Support
For issues, questions, or feature requests, please open an issue on GitHub.
