mirror of
https://github.com/xtr-dev/rondevu-client.git
synced 2025-12-13 12:23:24 +00:00
Implement crypto adapter pattern for platform independence
Adds CryptoAdapter interface with WebCryptoAdapter (browser) and
NodeCryptoAdapter (Node.js 19+) implementations.
Changes:
- Created crypto-adapter.ts interface
- Created web-crypto-adapter.ts for browser environments
- Created node-crypto-adapter.ts for Node.js environments
- Updated RondevuAPI to accept optional CryptoAdapter
- Updated Rondevu class to pass crypto adapter through
- Exported adapters and types in index.ts
- Updated README with platform support documentation
- Bumped version to 0.15.0
This allows the client library to work in both browser and Node.js
environments by providing platform-specific crypto implementations.
Example usage in Node.js:
import { Rondevu, NodeCryptoAdapter } from '@xtr-dev/rondevu-client'
const rondevu = new Rondevu({
apiUrl: 'https://api.ronde.vu',
cryptoAdapter: new NodeCryptoAdapter()
})
🤖 Generated with Claude Code
https://claude.com/claude-code
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
59
README.md
59
README.md
@@ -202,9 +202,62 @@ Main class for all Rondevu operations.
|
|||||||
import { Rondevu } from '@xtr-dev/rondevu-client'
|
import { Rondevu } from '@xtr-dev/rondevu-client'
|
||||||
|
|
||||||
const rondevu = new Rondevu({
|
const rondevu = new Rondevu({
|
||||||
apiUrl: string, // Signaling server URL
|
apiUrl: string, // Signaling server URL
|
||||||
username?: string, // Optional: your username (auto-generates anonymous if omitted)
|
username?: string, // Optional: your username (auto-generates anonymous if omitted)
|
||||||
keypair?: Keypair // Optional: reuse existing keypair
|
keypair?: Keypair, // Optional: reuse existing keypair
|
||||||
|
cryptoAdapter?: CryptoAdapter // Optional: platform-specific crypto (defaults to WebCryptoAdapter)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Platform Support (Browser & Node.js)
|
||||||
|
|
||||||
|
The client supports both browser and Node.js environments using crypto adapters:
|
||||||
|
|
||||||
|
**Browser (default):**
|
||||||
|
```typescript
|
||||||
|
import { Rondevu } from '@xtr-dev/rondevu-client'
|
||||||
|
|
||||||
|
// WebCryptoAdapter is used by default - no configuration needed
|
||||||
|
const rondevu = new Rondevu({
|
||||||
|
apiUrl: 'https://api.ronde.vu',
|
||||||
|
username: 'alice'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Node.js (19+ or 18 with --experimental-global-webcrypto):**
|
||||||
|
```typescript
|
||||||
|
import { Rondevu, NodeCryptoAdapter } from '@xtr-dev/rondevu-client'
|
||||||
|
|
||||||
|
const rondevu = new Rondevu({
|
||||||
|
apiUrl: 'https://api.ronde.vu',
|
||||||
|
username: 'alice',
|
||||||
|
cryptoAdapter: new NodeCryptoAdapter()
|
||||||
|
})
|
||||||
|
|
||||||
|
await rondevu.initialize()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Node.js support requires:
|
||||||
|
- Node.js 19+ (crypto.subtle available globally), OR
|
||||||
|
- Node.js 18 with `--experimental-global-webcrypto` flag
|
||||||
|
- WebRTC implementation like `wrtc` or `node-webrtc` for RTCPeerConnection
|
||||||
|
|
||||||
|
**Custom Crypto Adapter:**
|
||||||
|
```typescript
|
||||||
|
import { CryptoAdapter, Keypair } from '@xtr-dev/rondevu-client'
|
||||||
|
|
||||||
|
class CustomCryptoAdapter implements CryptoAdapter {
|
||||||
|
async generateKeypair(): Promise<Keypair> { /* ... */ }
|
||||||
|
async signMessage(message: string, privateKey: string): Promise<string> { /* ... */ }
|
||||||
|
async verifySignature(message: string, signature: string, publicKey: string): Promise<boolean> { /* ... */ }
|
||||||
|
bytesToBase64(bytes: Uint8Array): string { /* ... */ }
|
||||||
|
base64ToBytes(base64: string): Uint8Array { /* ... */ }
|
||||||
|
randomBytes(length: number): Uint8Array { /* ... */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
const rondevu = new Rondevu({
|
||||||
|
apiUrl: 'https://api.ronde.vu',
|
||||||
|
cryptoAdapter: new CustomCryptoAdapter()
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xtr-dev/rondevu-client",
|
"name": "@xtr-dev/rondevu-client",
|
||||||
"version": "0.14.0",
|
"version": "0.15.0",
|
||||||
"description": "TypeScript client for Rondevu with durable WebRTC connections, automatic reconnection, and message queuing",
|
"description": "TypeScript client for Rondevu with durable WebRTC connections, automatic reconnection, and message queuing",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
84
src/api.ts
84
src/api.ts
@@ -2,17 +2,10 @@
|
|||||||
* Rondevu API Client - RPC interface
|
* Rondevu API Client - RPC interface
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as ed25519 from '@noble/ed25519'
|
import { CryptoAdapter, Keypair } from './crypto-adapter.js'
|
||||||
|
import { WebCryptoAdapter } from './web-crypto-adapter.js'
|
||||||
|
|
||||||
// Set SHA-512 hash function for ed25519 (required in @noble/ed25519 v3+)
|
export type { Keypair } from './crypto-adapter.js'
|
||||||
ed25519.hashes.sha512Async = async (message: Uint8Array) => {
|
|
||||||
return new Uint8Array(await crypto.subtle.digest('SHA-512', message as BufferSource))
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Keypair {
|
|
||||||
publicKey: string
|
|
||||||
privateKey: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OfferRequest {
|
export interface OfferRequest {
|
||||||
sdp: string
|
sdp: string
|
||||||
@@ -47,22 +40,6 @@ export interface IceCandidate {
|
|||||||
createdAt: number
|
createdAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: Convert Uint8Array to base64 string
|
|
||||||
*/
|
|
||||||
function bytesToBase64(bytes: Uint8Array): string {
|
|
||||||
const binString = Array.from(bytes, byte => String.fromCodePoint(byte)).join('')
|
|
||||||
return btoa(binString)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: Convert base64 string to Uint8Array
|
|
||||||
*/
|
|
||||||
function base64ToBytes(base64: string): Uint8Array {
|
|
||||||
const binString = atob(base64)
|
|
||||||
return Uint8Array.from(binString, char => char.codePointAt(0)!)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RPC request format
|
* RPC request format
|
||||||
*/
|
*/
|
||||||
@@ -87,11 +64,17 @@ interface RpcResponse {
|
|||||||
* RondevuAPI - RPC-based API client for Rondevu signaling server
|
* RondevuAPI - RPC-based API client for Rondevu signaling server
|
||||||
*/
|
*/
|
||||||
export class RondevuAPI {
|
export class RondevuAPI {
|
||||||
|
private crypto: CryptoAdapter
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private baseUrl: string,
|
private baseUrl: string,
|
||||||
private username: string,
|
private username: string,
|
||||||
private keypair: Keypair
|
private keypair: Keypair,
|
||||||
) {}
|
cryptoAdapter?: CryptoAdapter
|
||||||
|
) {
|
||||||
|
// Use WebCryptoAdapter by default (browser environment)
|
||||||
|
this.crypto = cryptoAdapter || new WebCryptoAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate authentication parameters for RPC calls
|
* Generate authentication parameters for RPC calls
|
||||||
@@ -105,7 +88,7 @@ export class RondevuAPI {
|
|||||||
? `${method}:${this.username}:${params}:${timestamp}`
|
? `${method}:${this.username}:${params}:${timestamp}`
|
||||||
: `${method}:${this.username}:${timestamp}`
|
: `${method}:${this.username}:${timestamp}`
|
||||||
|
|
||||||
const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey)
|
const signature = await this.crypto.signMessage(message, this.keypair.privateKey)
|
||||||
|
|
||||||
return { message, signature }
|
return { message, signature }
|
||||||
}
|
}
|
||||||
@@ -163,47 +146,38 @@ export class RondevuAPI {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate an Ed25519 keypair for username claiming and service publishing
|
* Generate an Ed25519 keypair for username claiming and service publishing
|
||||||
|
* @param cryptoAdapter - Optional crypto adapter (defaults to WebCryptoAdapter)
|
||||||
*/
|
*/
|
||||||
static async generateKeypair(): Promise<Keypair> {
|
static async generateKeypair(cryptoAdapter?: CryptoAdapter): Promise<Keypair> {
|
||||||
const privateKey = ed25519.utils.randomSecretKey()
|
const adapter = cryptoAdapter || new WebCryptoAdapter()
|
||||||
const publicKey = await ed25519.getPublicKeyAsync(privateKey)
|
return await adapter.generateKeypair()
|
||||||
|
|
||||||
return {
|
|
||||||
publicKey: bytesToBase64(publicKey),
|
|
||||||
privateKey: bytesToBase64(privateKey),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign a message with an Ed25519 private key
|
* Sign a message with an Ed25519 private key
|
||||||
|
* @param cryptoAdapter - Optional crypto adapter (defaults to WebCryptoAdapter)
|
||||||
*/
|
*/
|
||||||
static async signMessage(message: string, privateKeyBase64: string): Promise<string> {
|
static async signMessage(
|
||||||
const privateKey = base64ToBytes(privateKeyBase64)
|
message: string,
|
||||||
const encoder = new TextEncoder()
|
privateKeyBase64: string,
|
||||||
const messageBytes = encoder.encode(message)
|
cryptoAdapter?: CryptoAdapter
|
||||||
const signature = await ed25519.signAsync(messageBytes, privateKey)
|
): Promise<string> {
|
||||||
|
const adapter = cryptoAdapter || new WebCryptoAdapter()
|
||||||
return bytesToBase64(signature)
|
return await adapter.signMessage(message, privateKeyBase64)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify an Ed25519 signature
|
* Verify an Ed25519 signature
|
||||||
|
* @param cryptoAdapter - Optional crypto adapter (defaults to WebCryptoAdapter)
|
||||||
*/
|
*/
|
||||||
static async verifySignature(
|
static async verifySignature(
|
||||||
message: string,
|
message: string,
|
||||||
signatureBase64: string,
|
signatureBase64: string,
|
||||||
publicKeyBase64: string
|
publicKeyBase64: string,
|
||||||
|
cryptoAdapter?: CryptoAdapter
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
const adapter = cryptoAdapter || new WebCryptoAdapter()
|
||||||
const signature = base64ToBytes(signatureBase64)
|
return await adapter.verifySignature(message, signatureBase64, publicKeyBase64)
|
||||||
const publicKey = base64ToBytes(publicKeyBase64)
|
|
||||||
const encoder = new TextEncoder()
|
|
||||||
const messageBytes = encoder.encode(message)
|
|
||||||
|
|
||||||
return await ed25519.verifyAsync(signature, messageBytes, publicKey)
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
48
src/crypto-adapter.ts
Normal file
48
src/crypto-adapter.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Crypto adapter interface for platform-independent cryptographic operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Keypair {
|
||||||
|
publicKey: string
|
||||||
|
privateKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platform-independent crypto adapter interface
|
||||||
|
* Implementations provide platform-specific crypto operations
|
||||||
|
*/
|
||||||
|
export interface CryptoAdapter {
|
||||||
|
/**
|
||||||
|
* Generate an Ed25519 keypair
|
||||||
|
*/
|
||||||
|
generateKeypair(): Promise<Keypair>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a message with an Ed25519 private key
|
||||||
|
*/
|
||||||
|
signMessage(message: string, privateKeyBase64: string): Promise<string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify an Ed25519 signature
|
||||||
|
*/
|
||||||
|
verifySignature(
|
||||||
|
message: string,
|
||||||
|
signatureBase64: string,
|
||||||
|
publicKeyBase64: string
|
||||||
|
): Promise<boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Uint8Array to base64 string
|
||||||
|
*/
|
||||||
|
bytesToBase64(bytes: Uint8Array): string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert base64 string to Uint8Array
|
||||||
|
*/
|
||||||
|
base64ToBytes(base64: string): Uint8Array
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate random bytes
|
||||||
|
*/
|
||||||
|
randomBytes(length: number): Uint8Array
|
||||||
|
}
|
||||||
@@ -7,6 +7,10 @@ export { Rondevu } from './rondevu.js'
|
|||||||
export { RondevuAPI } from './api.js'
|
export { RondevuAPI } from './api.js'
|
||||||
export { RondevuSignaler } from './rondevu-signaler.js'
|
export { RondevuSignaler } from './rondevu-signaler.js'
|
||||||
|
|
||||||
|
// Export crypto adapters
|
||||||
|
export { WebCryptoAdapter } from './web-crypto-adapter.js'
|
||||||
|
export { NodeCryptoAdapter } from './node-crypto-adapter.js'
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
export type {
|
export type {
|
||||||
Signaler,
|
Signaler,
|
||||||
@@ -26,3 +30,5 @@ export type { RondevuOptions, PublishServiceOptions } from './rondevu.js'
|
|||||||
|
|
||||||
export type { PollingConfig } from './rondevu-signaler.js'
|
export type { PollingConfig } from './rondevu-signaler.js'
|
||||||
|
|
||||||
|
export type { CryptoAdapter } from './crypto-adapter.js'
|
||||||
|
|
||||||
|
|||||||
98
src/node-crypto-adapter.ts
Normal file
98
src/node-crypto-adapter.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Node.js Crypto adapter for Node.js environments
|
||||||
|
* Requires Node.js 19+ or Node.js 18 with --experimental-global-webcrypto flag
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as ed25519 from '@noble/ed25519'
|
||||||
|
import { CryptoAdapter, Keypair } from './crypto-adapter.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node.js Crypto implementation using Node.js built-in APIs
|
||||||
|
* Uses Buffer for base64 encoding and crypto.randomBytes for random generation
|
||||||
|
*
|
||||||
|
* Requirements:
|
||||||
|
* - Node.js 19+ (crypto.subtle available globally)
|
||||||
|
* - OR Node.js 18 with --experimental-global-webcrypto flag
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { RondevuAPI } from '@xtr-dev/rondevu-client'
|
||||||
|
* import { NodeCryptoAdapter } from '@xtr-dev/rondevu-client/node'
|
||||||
|
*
|
||||||
|
* const api = new RondevuAPI(
|
||||||
|
* 'https://signal.example.com',
|
||||||
|
* 'alice',
|
||||||
|
* keypair,
|
||||||
|
* new NodeCryptoAdapter()
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class NodeCryptoAdapter implements CryptoAdapter {
|
||||||
|
constructor() {
|
||||||
|
// Set SHA-512 hash function for ed25519 using Node's crypto.subtle
|
||||||
|
if (typeof crypto === 'undefined' || !crypto.subtle) {
|
||||||
|
throw new Error(
|
||||||
|
'crypto.subtle is not available. ' +
|
||||||
|
'Node.js 19+ is required, or Node.js 18 with --experimental-global-webcrypto flag'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ed25519.hashes.sha512Async = async (message: Uint8Array) => {
|
||||||
|
const hash = await crypto.subtle.digest('SHA-512', message as BufferSource)
|
||||||
|
return new Uint8Array(hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateKeypair(): Promise<Keypair> {
|
||||||
|
const privateKey = ed25519.utils.randomSecretKey()
|
||||||
|
const publicKey = await ed25519.getPublicKeyAsync(privateKey)
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey: this.bytesToBase64(publicKey),
|
||||||
|
privateKey: this.bytesToBase64(privateKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async signMessage(message: string, privateKeyBase64: string): Promise<string> {
|
||||||
|
const privateKey = this.base64ToBytes(privateKeyBase64)
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const messageBytes = encoder.encode(message)
|
||||||
|
const signature = await ed25519.signAsync(messageBytes, privateKey)
|
||||||
|
|
||||||
|
return this.bytesToBase64(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifySignature(
|
||||||
|
message: string,
|
||||||
|
signatureBase64: string,
|
||||||
|
publicKeyBase64: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const signature = this.base64ToBytes(signatureBase64)
|
||||||
|
const publicKey = this.base64ToBytes(publicKeyBase64)
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const messageBytes = encoder.encode(message)
|
||||||
|
|
||||||
|
return await ed25519.verifyAsync(signature, messageBytes, publicKey)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesToBase64(bytes: Uint8Array): string {
|
||||||
|
// Node.js Buffer provides native base64 encoding
|
||||||
|
// @ts-expect-error - Buffer is available in Node.js but not in browser TypeScript definitions
|
||||||
|
return Buffer.from(bytes).toString('base64')
|
||||||
|
}
|
||||||
|
|
||||||
|
base64ToBytes(base64: string): Uint8Array {
|
||||||
|
// Node.js Buffer provides native base64 decoding
|
||||||
|
// @ts-expect-error - Buffer is available in Node.js but not in browser TypeScript definitions
|
||||||
|
return new Uint8Array(Buffer.from(base64, 'base64'))
|
||||||
|
}
|
||||||
|
|
||||||
|
randomBytes(length: number): Uint8Array {
|
||||||
|
// Use Web Crypto API's getRandomValues (available in Node 19+)
|
||||||
|
return crypto.getRandomValues(new Uint8Array(length))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { RondevuAPI, Keypair, Service, ServiceRequest, IceCandidate } from './api.js'
|
import { RondevuAPI, Keypair, Service, ServiceRequest, IceCandidate } from './api.js'
|
||||||
|
import { CryptoAdapter } from './crypto-adapter.js'
|
||||||
|
|
||||||
export interface RondevuOptions {
|
export interface RondevuOptions {
|
||||||
apiUrl: string
|
apiUrl: string
|
||||||
username?: string // Optional, will generate anonymous if not provided
|
username?: string // Optional, will generate anonymous if not provided
|
||||||
keypair?: Keypair // Optional, will generate if not provided
|
keypair?: Keypair // Optional, will generate if not provided
|
||||||
|
cryptoAdapter?: CryptoAdapter // Optional, defaults to WebCryptoAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PublishServiceOptions {
|
export interface PublishServiceOptions {
|
||||||
@@ -52,11 +54,13 @@ export class Rondevu {
|
|||||||
private username: string
|
private username: string
|
||||||
private keypair: Keypair | null = null
|
private keypair: Keypair | null = null
|
||||||
private usernameClaimed = false
|
private usernameClaimed = false
|
||||||
|
private cryptoAdapter?: CryptoAdapter
|
||||||
|
|
||||||
constructor(options: RondevuOptions) {
|
constructor(options: RondevuOptions) {
|
||||||
this.apiUrl = options.apiUrl
|
this.apiUrl = options.apiUrl
|
||||||
this.username = options.username || this.generateAnonymousUsername()
|
this.username = options.username || this.generateAnonymousUsername()
|
||||||
this.keypair = options.keypair || null
|
this.keypair = options.keypair || null
|
||||||
|
this.cryptoAdapter = options.cryptoAdapter
|
||||||
|
|
||||||
console.log('[Rondevu] Constructor called:', {
|
console.log('[Rondevu] Constructor called:', {
|
||||||
username: this.username,
|
username: this.username,
|
||||||
@@ -89,14 +93,14 @@ export class Rondevu {
|
|||||||
// Generate keypair if not provided
|
// Generate keypair if not provided
|
||||||
if (!this.keypair) {
|
if (!this.keypair) {
|
||||||
console.log('[Rondevu] Generating new keypair...')
|
console.log('[Rondevu] Generating new keypair...')
|
||||||
this.keypair = await RondevuAPI.generateKeypair()
|
this.keypair = await RondevuAPI.generateKeypair(this.cryptoAdapter)
|
||||||
console.log('[Rondevu] Generated keypair, publicKey:', this.keypair.publicKey)
|
console.log('[Rondevu] Generated keypair, publicKey:', this.keypair.publicKey)
|
||||||
} else {
|
} else {
|
||||||
console.log('[Rondevu] Using existing keypair, publicKey:', this.keypair.publicKey)
|
console.log('[Rondevu] Using existing keypair, publicKey:', this.keypair.publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create API instance with username and keypair
|
// Create API instance with username, keypair, and crypto adapter
|
||||||
this.api = new RondevuAPI(this.apiUrl, this.username, this.keypair)
|
this.api = new RondevuAPI(this.apiUrl, this.username, this.keypair, this.cryptoAdapter)
|
||||||
console.log('[Rondevu] Created API instance with username:', this.username)
|
console.log('[Rondevu] Created API instance with username:', this.username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
67
src/web-crypto-adapter.ts
Normal file
67
src/web-crypto-adapter.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Web Crypto adapter for browser environments
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as ed25519 from '@noble/ed25519'
|
||||||
|
import { CryptoAdapter, Keypair } from './crypto-adapter.js'
|
||||||
|
|
||||||
|
// Set SHA-512 hash function for ed25519 (required in @noble/ed25519 v3+)
|
||||||
|
ed25519.hashes.sha512Async = async (message: Uint8Array) => {
|
||||||
|
return new Uint8Array(await crypto.subtle.digest('SHA-512', message as BufferSource))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web Crypto implementation using browser APIs
|
||||||
|
* Uses btoa/atob for base64 encoding and crypto.getRandomValues for random bytes
|
||||||
|
*/
|
||||||
|
export class WebCryptoAdapter implements CryptoAdapter {
|
||||||
|
async generateKeypair(): Promise<Keypair> {
|
||||||
|
const privateKey = ed25519.utils.randomSecretKey()
|
||||||
|
const publicKey = await ed25519.getPublicKeyAsync(privateKey)
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey: this.bytesToBase64(publicKey),
|
||||||
|
privateKey: this.bytesToBase64(privateKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async signMessage(message: string, privateKeyBase64: string): Promise<string> {
|
||||||
|
const privateKey = this.base64ToBytes(privateKeyBase64)
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const messageBytes = encoder.encode(message)
|
||||||
|
const signature = await ed25519.signAsync(messageBytes, privateKey)
|
||||||
|
|
||||||
|
return this.bytesToBase64(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifySignature(
|
||||||
|
message: string,
|
||||||
|
signatureBase64: string,
|
||||||
|
publicKeyBase64: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const signature = this.base64ToBytes(signatureBase64)
|
||||||
|
const publicKey = this.base64ToBytes(publicKeyBase64)
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const messageBytes = encoder.encode(message)
|
||||||
|
|
||||||
|
return await ed25519.verifyAsync(signature, messageBytes, publicKey)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesToBase64(bytes: Uint8Array): string {
|
||||||
|
const binString = Array.from(bytes, byte => String.fromCodePoint(byte)).join('')
|
||||||
|
return btoa(binString)
|
||||||
|
}
|
||||||
|
|
||||||
|
base64ToBytes(base64: string): Uint8Array {
|
||||||
|
const binString = atob(base64)
|
||||||
|
return Uint8Array.from(binString, char => char.codePointAt(0)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
randomBytes(length: number): Uint8Array {
|
||||||
|
return crypto.getRandomValues(new Uint8Array(length))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user