Browser-compatible authentication and key management for cryptocurrency wallets
npm install @cryptforge/authBrowser-compatible authentication and key management for cryptocurrency wallets. Built on industry-standard BIP39/BIP44 with secure keystore encryption.
- 🔐 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
``bash`
npm install @cryptforge/auth @cryptforge/blockchain-evm @cryptforge/blockchain-btc
`typescript
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);
`
#### 1. Generate and Backup Mnemonic
`typescript
// 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 mnemonic
`
#### 2. Create Identity
`typescript
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 use
`
#### 1. List Available Identities
`typescript`
const identities = await auth.listIdentities();
// [
// {
// id: 'identity_A1B2C3D4',
// label: 'Personal Wallet',
// fingerprint: 'A1B2C3D4',
// createdAt: Date,
// lastAccess: Date
// }
// ]
#### 2. Select and Unlock
`typescript
// 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");
`
#### Create Identity
`typescript`
const { identity, keys } = await auth.createIdentity({
mnemonic: "word1 word2 ... word12",
password: "secure-password",
label: "Trading Wallet",
metadata: { purpose: "trading" },
chainId: "ethereum", // Optional
});
#### Import Identity
`typescript
// 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
`typescript
// 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
`typescript`
// Permanently delete (requires password)
await auth.deleteIdentity(identity.id, "password");
// ⚠️ Make sure mnemonic is backed up first!
#### Update Identity
`typescript`
await auth.updateIdentity(identity.id, {
label: "Updated Wallet Name",
metadata: { theme: "dark" },
});
#### Change Password
`typescript`
await auth.changePassword(identity.id, "old-password", "new-password");
#### Unlock
`typescript
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
`typescript`
await auth.lock();
// Clears keys from memory
// Keystore remains encrypted in IndexedDB
#### Switch Chain
`typescript
// Switch to different blockchain (if already unlocked)
await auth.switchChain("bitcoin");
// Switch when locked (requires password)
await auth.switchChain("bitcoin", "password");
`
#### Get Addresses
`typescript
// 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)
`typescript`
// 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 20
#### Sign Message
`typescript
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
`typescript
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
`typescript`
const isValid = await auth.verifySignature(
"Hello CryptForge!",
signature,
publicKey
);
#### Rotate Keys
`typescript
// 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
`typescript`
// 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
});
`typescript
// 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[]
`
`typescript
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();
`
- 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
`typescript
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,
},
},
})
);
`
Implement the BlockchainAdapter interface to add support for new blockchains:
`typescript
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
// Implement Solana key derivation
}
async deriveKeysAtIndex(mnemonic: string, index: number): Promise
// Implement indexed derivation
}
// ... implement all required methods
}
// Register your custom adapter
auth.registerAdapter("solana", new SolanaAdapter());
`
#### 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 | 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 | Delete identity |changePassword
| | identityId: string, oldPassword: string, newPassword: string | Promise | Change password |
#### Session Management
| Method | Parameters | Returns | Description |
| -------- | --------------- | ------------------- | ---------------------- |
| unlock | UnlockOptions | Promise<{ keys }> | Decrypt and load keys |lock
| | - | Promise | 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 | 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 | Get multiple addresses |findUsedAddresses
| | chainId: string, checkBalance: Function | Promise | Account discovery |
#### Event Management
| Method | Parameters | Returns | Description |
| ------------------- | ------------------------------ | ------------ | ----------------------------------------- |
| onAuthStateChange | callback: AuthChangeCallback | () => void | Subscribe to events (returns unsubscribe) |
#### CreateIdentityOptions
`typescript`
interface CreateIdentityOptions {
mnemonic: string; // BIP39 mnemonic (12 or 24 words)
password: string; // Password to encrypt keystore
label?: string; // Human-readable label
metadata?: Record
chainId?: string; // Optional: unlock with specific chain
}
#### UnlockOptions
`typescript`
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
`typescript`
interface Identity {
id: string; // Unique identifier
publicKey: string; // Master public key (xpub)
fingerprint: string; // Master key fingerprint
label?: string; // User-defined label
metadata: Record
createdAt: Date;
lastAccess?: Date;
}
#### Keys
`typescript`
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
`typescript`
interface Chain {
id: string; // Chain identifier (e.g., 'ethereum')
name: string; // Display name (e.g., 'Ethereum')
symbol: string; // Token symbol (e.g., 'ETH')
}
`typescript
// 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, ... });
`
`typescript`
// 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)
);
}
`typescript`
// Always use auto-lock for security
await auth.unlock({
password,
chainId: "ethereum",
duration: 5 60 1000, // Lock after 5 minutes of inactivity
});
`typescript
// 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
}
});
`
`typescript
// 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();
});
`
`typescript
// 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" });
`
`typescript
// 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",
});
`
`typescript
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);
});
`
Get the master public key derived from the mnemonic.
This returns ONLY the public key bytes (33 bytes, compressed secp256k1), NOT the extended public key (xpub). This is safe to expose publicly because:
1. ✅ Cannot derive child keys (no chaincode included)
2. ✅ Cannot compute private keys
3. ✅ Standard public key cryptography - public keys are meant to be public
4. ✅ Chain-independent - same key regardless of blockchain
- Document ownership verification
- Identity verification across different blockchains
- Signature verification
- Access control (e.g., "owner" field in documents)
- ❌ 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
`typescript
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 chain
`
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.subtle for encryptionindexedDB
- for storageUint8Array
- for binary data
`json`
{
"@scure/bip39": "^1.2.1",
"@scure/bip32": "^1.3.2",
"@cryptforge/core": "workspace:*"
}
All dependencies are browser-safe, audited, and actively maintained.
- DATA_ENCRYPTION.md - Detailed documentation on HKDF data encryption and master public key features for chain-independent encryption and identity verification
See the complete working example in examples/vue-electron-example/src/AuthTest.vue.
MIT
Contributions welcome! Please ensure:
- All code is browser-compatible (use @scure and @noble` libraries)
- TypeScript types are properly defined
- Examples are updated
- Tests pass (when implemented)
For issues, questions, or feature requests, please open an issue on GitHub.