@bananalink-sdk/protocol 1.2.7
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/README.md +604 -0
- package/dist/chunk-32OWUOZ3.js +308 -0
- package/dist/chunk-32OWUOZ3.js.map +1 -0
- package/dist/chunk-65HNHRJK.cjs +123 -0
- package/dist/chunk-65HNHRJK.cjs.map +1 -0
- package/dist/chunk-7KYDLL3B.js +480 -0
- package/dist/chunk-7KYDLL3B.js.map +1 -0
- package/dist/chunk-A6FLEJ7R.cjs +62 -0
- package/dist/chunk-A6FLEJ7R.cjs.map +1 -0
- package/dist/chunk-CUJK7ZTS.js +217 -0
- package/dist/chunk-CUJK7ZTS.js.map +1 -0
- package/dist/chunk-GI3BUPIH.cjs +236 -0
- package/dist/chunk-GI3BUPIH.cjs.map +1 -0
- package/dist/chunk-JXHV66Q4.js +106 -0
- package/dist/chunk-JXHV66Q4.js.map +1 -0
- package/dist/chunk-KNGZKGRS.cjs +552 -0
- package/dist/chunk-KNGZKGRS.cjs.map +1 -0
- package/dist/chunk-LELPCIE7.js +840 -0
- package/dist/chunk-LELPCIE7.js.map +1 -0
- package/dist/chunk-MCZG7QEM.cjs +310 -0
- package/dist/chunk-MCZG7QEM.cjs.map +1 -0
- package/dist/chunk-TCVKC227.js +56 -0
- package/dist/chunk-TCVKC227.js.map +1 -0
- package/dist/chunk-VXLUSU5B.cjs +856 -0
- package/dist/chunk-VXLUSU5B.cjs.map +1 -0
- package/dist/chunk-WCQVDF3K.js +12 -0
- package/dist/chunk-WCQVDF3K.js.map +1 -0
- package/dist/chunk-WGEGR3DF.cjs +15 -0
- package/dist/chunk-WGEGR3DF.cjs.map +1 -0
- package/dist/client-session-claim-3QF3noOr.d.ts +197 -0
- package/dist/client-session-claim-C4lUik3b.d.cts +197 -0
- package/dist/core-DMhuNfoz.d.cts +62 -0
- package/dist/core-DMhuNfoz.d.ts +62 -0
- package/dist/crypto/providers/noble-provider.cjs +14 -0
- package/dist/crypto/providers/noble-provider.cjs.map +1 -0
- package/dist/crypto/providers/noble-provider.d.cts +30 -0
- package/dist/crypto/providers/noble-provider.d.ts +30 -0
- package/dist/crypto/providers/noble-provider.js +5 -0
- package/dist/crypto/providers/noble-provider.js.map +1 -0
- package/dist/crypto/providers/node-provider.cjs +308 -0
- package/dist/crypto/providers/node-provider.cjs.map +1 -0
- package/dist/crypto/providers/node-provider.d.cts +32 -0
- package/dist/crypto/providers/node-provider.d.ts +32 -0
- package/dist/crypto/providers/node-provider.js +306 -0
- package/dist/crypto/providers/node-provider.js.map +1 -0
- package/dist/crypto/providers/quickcrypto-provider.cjs +339 -0
- package/dist/crypto/providers/quickcrypto-provider.cjs.map +1 -0
- package/dist/crypto/providers/quickcrypto-provider.d.cts +34 -0
- package/dist/crypto/providers/quickcrypto-provider.d.ts +34 -0
- package/dist/crypto/providers/quickcrypto-provider.js +337 -0
- package/dist/crypto/providers/quickcrypto-provider.js.map +1 -0
- package/dist/crypto/providers/webcrypto-provider.cjs +310 -0
- package/dist/crypto/providers/webcrypto-provider.cjs.map +1 -0
- package/dist/crypto/providers/webcrypto-provider.d.cts +30 -0
- package/dist/crypto/providers/webcrypto-provider.d.ts +30 -0
- package/dist/crypto/providers/webcrypto-provider.js +308 -0
- package/dist/crypto/providers/webcrypto-provider.js.map +1 -0
- package/dist/crypto-BUS06Qz-.d.cts +40 -0
- package/dist/crypto-BUS06Qz-.d.ts +40 -0
- package/dist/crypto-export.cjs +790 -0
- package/dist/crypto-export.cjs.map +1 -0
- package/dist/crypto-export.d.cts +257 -0
- package/dist/crypto-export.d.ts +257 -0
- package/dist/crypto-export.js +709 -0
- package/dist/crypto-export.js.map +1 -0
- package/dist/crypto-provider-deYoVIxi.d.cts +36 -0
- package/dist/crypto-provider-deYoVIxi.d.ts +36 -0
- package/dist/index.cjs +615 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +379 -0
- package/dist/index.d.ts +379 -0
- package/dist/index.js +504 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas-export.cjs +294 -0
- package/dist/schemas-export.cjs.map +1 -0
- package/dist/schemas-export.d.cts +1598 -0
- package/dist/schemas-export.d.ts +1598 -0
- package/dist/schemas-export.js +5 -0
- package/dist/schemas-export.js.map +1 -0
- package/dist/siwe-export.cjs +237 -0
- package/dist/siwe-export.cjs.map +1 -0
- package/dist/siwe-export.d.cts +27 -0
- package/dist/siwe-export.d.ts +27 -0
- package/dist/siwe-export.js +228 -0
- package/dist/siwe-export.js.map +1 -0
- package/dist/testing.cjs +54 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +20 -0
- package/dist/testing.d.ts +20 -0
- package/dist/testing.js +51 -0
- package/dist/testing.js.map +1 -0
- package/dist/validation-export.cjs +359 -0
- package/dist/validation-export.cjs.map +1 -0
- package/dist/validation-export.d.cts +3 -0
- package/dist/validation-export.d.ts +3 -0
- package/dist/validation-export.js +6 -0
- package/dist/validation-export.js.map +1 -0
- package/dist/validators-export.cjs +73 -0
- package/dist/validators-export.cjs.map +1 -0
- package/dist/validators-export.d.cts +37 -0
- package/dist/validators-export.d.ts +37 -0
- package/dist/validators-export.js +4 -0
- package/dist/validators-export.js.map +1 -0
- package/package.json +140 -0
- package/src/constants/index.ts +205 -0
- package/src/crypto/context.ts +228 -0
- package/src/crypto/diagnostics.ts +772 -0
- package/src/crypto/errors.ts +114 -0
- package/src/crypto/index.ts +89 -0
- package/src/crypto/payload-handler.ts +102 -0
- package/src/crypto/providers/compliance-provider.ts +579 -0
- package/src/crypto/providers/factory.ts +204 -0
- package/src/crypto/providers/index.ts +44 -0
- package/src/crypto/providers/noble-provider.ts +392 -0
- package/src/crypto/providers/node-provider.ts +433 -0
- package/src/crypto/providers/quickcrypto-provider.ts +483 -0
- package/src/crypto/providers/registry.ts +129 -0
- package/src/crypto/providers/webcrypto-provider.ts +364 -0
- package/src/crypto/session-security.ts +185 -0
- package/src/crypto/types.ts +93 -0
- package/src/crypto/utils.ts +190 -0
- package/src/crypto-export.ts +21 -0
- package/src/index.ts +38 -0
- package/src/schemas/auth.ts +60 -0
- package/src/schemas/client-messages.ts +57 -0
- package/src/schemas/core.ts +144 -0
- package/src/schemas/crypto.ts +65 -0
- package/src/schemas/discovery.ts +79 -0
- package/src/schemas/index.ts +239 -0
- package/src/schemas/relay-messages.ts +45 -0
- package/src/schemas/wallet-messages.ts +177 -0
- package/src/schemas-export.ts +23 -0
- package/src/siwe-export.ts +27 -0
- package/src/testing.ts +71 -0
- package/src/types/auth.ts +60 -0
- package/src/types/client-messages.ts +84 -0
- package/src/types/core.ts +131 -0
- package/src/types/crypto-provider.ts +264 -0
- package/src/types/crypto.ts +90 -0
- package/src/types/discovery.ts +50 -0
- package/src/types/errors.ts +87 -0
- package/src/types/index.ts +197 -0
- package/src/types/post-auth-operations.ts +363 -0
- package/src/types/providers.ts +72 -0
- package/src/types/relay-messages.ts +60 -0
- package/src/types/request-lifecycle.ts +161 -0
- package/src/types/signing-operations.ts +99 -0
- package/src/types/wallet-messages.ts +251 -0
- package/src/utils/client-session-claim.ts +188 -0
- package/src/utils/index.ts +54 -0
- package/src/utils/public-keys.ts +49 -0
- package/src/utils/siwe.ts +362 -0
- package/src/utils/url-decoding.ts +126 -0
- package/src/utils/url-encoding.ts +144 -0
- package/src/utils/wallet-session-claim.ts +188 -0
- package/src/validation-export.ts +32 -0
- package/src/validators/index.ts +222 -0
- package/src/validators-export.ts +8 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { Logger } from '@bananalink-sdk/logger';
|
|
2
|
+
import type { CryptoProvider, CryptoProviderType } from '../../types/crypto-provider';
|
|
3
|
+
import { CryptoProviderUnavailableError } from '../errors';
|
|
4
|
+
import { detectPlatform } from '../utils';
|
|
5
|
+
import {
|
|
6
|
+
getCryptoProviderFactory,
|
|
7
|
+
getRegisteredCryptoProviders
|
|
8
|
+
} from './registry';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate context-appropriate recommendations for unavailable providers
|
|
12
|
+
*/
|
|
13
|
+
function generateRecommendations(
|
|
14
|
+
requestedProvider: CryptoProviderType,
|
|
15
|
+
platform: ReturnType<typeof detectPlatform>
|
|
16
|
+
): string[] {
|
|
17
|
+
const recommendations: string[] = [];
|
|
18
|
+
|
|
19
|
+
// Provider-specific recommendations
|
|
20
|
+
if (requestedProvider === 'webcrypto') {
|
|
21
|
+
recommendations.push("WebCrypto requires a browser or Node.js 15+ environment");
|
|
22
|
+
if (platform.isNode) {
|
|
23
|
+
recommendations.push("Try 'node' provider for Node.js");
|
|
24
|
+
}
|
|
25
|
+
} else if (requestedProvider === 'node') {
|
|
26
|
+
recommendations.push("Node crypto requires Node.js environment");
|
|
27
|
+
if (platform.isBrowser) {
|
|
28
|
+
recommendations.push("Try 'webcrypto' provider for browsers");
|
|
29
|
+
}
|
|
30
|
+
} else if (requestedProvider === 'quickcrypto') {
|
|
31
|
+
recommendations.push("QuickCrypto requires React Native environment");
|
|
32
|
+
if (!platform.isReactNative) {
|
|
33
|
+
if (platform.isNode) {
|
|
34
|
+
recommendations.push("Try 'node' provider for Node.js");
|
|
35
|
+
} else if (platform.isBrowser) {
|
|
36
|
+
recommendations.push("Try 'webcrypto' provider for browsers");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Universal fallback
|
|
42
|
+
recommendations.push("'noble' provider works in all environments as a fallback");
|
|
43
|
+
recommendations.push(`Import the provider: import '@bananalink-sdk/protocol/crypto/provider/${requestedProvider}'`);
|
|
44
|
+
|
|
45
|
+
return recommendations;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a crypto provider based on type
|
|
50
|
+
*
|
|
51
|
+
* This factory function uses the global registry to instantiate providers.
|
|
52
|
+
* Providers must be explicitly imported before they can be used.
|
|
53
|
+
*
|
|
54
|
+
* @param preferredProvider - Optional preferred provider type
|
|
55
|
+
* @param logger - Optional logger for debug output
|
|
56
|
+
* @param strict - If true, throw error if preferred provider unavailable
|
|
57
|
+
* @returns The created crypto provider instance
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* // Import providers to make them available
|
|
62
|
+
* import '@bananalink-sdk/protocol/crypto/provider/noble';
|
|
63
|
+
* import '@bananalink-sdk/protocol/crypto/provider/webcrypto';
|
|
64
|
+
*
|
|
65
|
+
* // Create provider
|
|
66
|
+
* const provider = createCryptoProvider('noble');
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function createCryptoProvider(
|
|
70
|
+
preferredProvider?: CryptoProviderType,
|
|
71
|
+
logger?: Logger,
|
|
72
|
+
strict?: boolean
|
|
73
|
+
): CryptoProvider {
|
|
74
|
+
const registeredProviders = getRegisteredCryptoProviders();
|
|
75
|
+
|
|
76
|
+
logger?.debug('Creating crypto provider', {
|
|
77
|
+
preferredProvider,
|
|
78
|
+
strict,
|
|
79
|
+
registeredProviders
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Determine which provider to use
|
|
83
|
+
let providerType: CryptoProviderType | undefined = preferredProvider;
|
|
84
|
+
|
|
85
|
+
// Strict validation: If no provider specified, require exactly one registered
|
|
86
|
+
if (!providerType) {
|
|
87
|
+
if (registeredProviders.length === 0) {
|
|
88
|
+
throw new CryptoProviderUnavailableError(
|
|
89
|
+
'No crypto providers are registered. Did you forget to import a provider?',
|
|
90
|
+
{
|
|
91
|
+
availableProviders: [],
|
|
92
|
+
recommendations: [
|
|
93
|
+
"Import at least one crypto provider, e.g.: import '@bananalink-sdk/protocol/crypto/provider/noble'",
|
|
94
|
+
"Noble provider is recommended as a universal fallback"
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
} else if (registeredProviders.length === 1) {
|
|
99
|
+
// Auto-select the single registered provider
|
|
100
|
+
providerType = registeredProviders[0];
|
|
101
|
+
logger?.debug(`Auto-selected '${providerType}' provider (only one registered)`);
|
|
102
|
+
} else {
|
|
103
|
+
// Multiple registered - require explicit selection
|
|
104
|
+
throw new CryptoProviderUnavailableError(
|
|
105
|
+
`Multiple crypto providers registered: ${registeredProviders.join(', ')}. ` +
|
|
106
|
+
`Either import only one provider OR explicitly set cryptoProvider.type. ` +
|
|
107
|
+
`Example: cryptoProvider: { type: '${registeredProviders[0]}' }`,
|
|
108
|
+
{
|
|
109
|
+
availableProviders: registeredProviders,
|
|
110
|
+
recommendations: [
|
|
111
|
+
`Import only one crypto provider, or specify which one to use`,
|
|
112
|
+
`Registered providers: ${registeredProviders.join(', ')}`,
|
|
113
|
+
`Set cryptoProvider.type to one of the registered providers`
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Try to create the provider
|
|
121
|
+
try {
|
|
122
|
+
const factory = getCryptoProviderFactory(providerType);
|
|
123
|
+
const provider = factory(logger);
|
|
124
|
+
|
|
125
|
+
logger?.info(`Using ${provider.name} crypto provider`, {
|
|
126
|
+
providerType,
|
|
127
|
+
isAvailable: provider.isAvailable
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return provider;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// If strict mode AND user explicitly requested a specific provider, throw immediately
|
|
133
|
+
const userRequestedSpecificProvider = preferredProvider !== undefined;
|
|
134
|
+
if (strict && userRequestedSpecificProvider) {
|
|
135
|
+
// If already a CryptoProviderUnavailableError, re-throw as-is
|
|
136
|
+
if (error instanceof CryptoProviderUnavailableError) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Wrap generic errors with full context
|
|
141
|
+
const platform = detectPlatform();
|
|
142
|
+
const availableProviders = getRegisteredCryptoProviders();
|
|
143
|
+
const recommendations = generateRecommendations(providerType, platform);
|
|
144
|
+
|
|
145
|
+
throw new CryptoProviderUnavailableError(
|
|
146
|
+
`Strict mode: Crypto provider '${providerType}' failed to initialize: ${error instanceof Error ? error.message : String(error)}`,
|
|
147
|
+
{
|
|
148
|
+
requestedProvider: providerType,
|
|
149
|
+
availableProviders,
|
|
150
|
+
platform,
|
|
151
|
+
recommendations,
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Try fallback to any available provider
|
|
157
|
+
logger?.warn(`Failed to create '${providerType}' provider, trying fallback`, {
|
|
158
|
+
error: error instanceof Error ? { message: error.message } : { message: String(error) }
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Try each registered provider as fallback
|
|
162
|
+
for (const fallbackType of registeredProviders) {
|
|
163
|
+
if (fallbackType === providerType) continue; // Skip the one that failed
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const factory = getCryptoProviderFactory(fallbackType);
|
|
167
|
+
const provider = factory(logger);
|
|
168
|
+
|
|
169
|
+
logger?.info(`Using ${provider.name} crypto provider (fallback)`, {
|
|
170
|
+
originalType: providerType,
|
|
171
|
+
fallbackType
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return provider;
|
|
175
|
+
} catch (fallbackError) {
|
|
176
|
+
logger?.debug(`Fallback to '${fallbackType}' failed`, {
|
|
177
|
+
error: fallbackError instanceof Error ? { message: fallbackError.message } : { message: String(fallbackError) }
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// If we get here, no providers worked
|
|
183
|
+
const platform = detectPlatform();
|
|
184
|
+
const recommendations = generateRecommendations(providerType, platform);
|
|
185
|
+
|
|
186
|
+
throw new CryptoProviderUnavailableError(
|
|
187
|
+
`Failed to create any crypto provider. Requested: '${providerType}'`,
|
|
188
|
+
{
|
|
189
|
+
requestedProvider: providerType,
|
|
190
|
+
availableProviders: registeredProviders,
|
|
191
|
+
platform,
|
|
192
|
+
recommendations,
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Export registry functions for advanced usage
|
|
200
|
+
*/
|
|
201
|
+
export {
|
|
202
|
+
isCryptoProviderRegistered,
|
|
203
|
+
getRegisteredCryptoProviders
|
|
204
|
+
} from './registry';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto providers for environment-agnostic cryptographic operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Types
|
|
6
|
+
export type {
|
|
7
|
+
CryptoProvider,
|
|
8
|
+
CryptoKeyLike,
|
|
9
|
+
ProviderKeyPair,
|
|
10
|
+
ProviderDetectionResult,
|
|
11
|
+
CryptoProviderType,
|
|
12
|
+
PlatformDetectionResult
|
|
13
|
+
} from '../../types/crypto-provider';
|
|
14
|
+
|
|
15
|
+
// Compliance types
|
|
16
|
+
export type {
|
|
17
|
+
ComplianceEventType,
|
|
18
|
+
ComplianceAuditEvent,
|
|
19
|
+
ComplianceAuditor,
|
|
20
|
+
KeyUsageRestrictions,
|
|
21
|
+
ComplianceConfig
|
|
22
|
+
} from './compliance-provider';
|
|
23
|
+
|
|
24
|
+
// Factory functions
|
|
25
|
+
export {
|
|
26
|
+
createCryptoProvider,
|
|
27
|
+
isCryptoProviderRegistered,
|
|
28
|
+
getRegisteredCryptoProviders,
|
|
29
|
+
} from './factory';
|
|
30
|
+
|
|
31
|
+
// Registry functions (for advanced usage)
|
|
32
|
+
export {
|
|
33
|
+
registerCryptoProvider,
|
|
34
|
+
getCryptoProviderFactory,
|
|
35
|
+
clearCryptoProviderRegistry,
|
|
36
|
+
type CryptoProviderFactory
|
|
37
|
+
} from './registry';
|
|
38
|
+
|
|
39
|
+
// Note: Provider implementations should be imported from specific subpaths for explicit loading:
|
|
40
|
+
// - './webcrypto-provider' for WebCryptoProvider
|
|
41
|
+
// - './node-provider' for NodeCryptoProvider
|
|
42
|
+
// - './noble-provider' for NobleCryptoProvider
|
|
43
|
+
// - './quickcrypto-provider' for QuickCryptoProvider
|
|
44
|
+
// - './compliance-provider' for ComplianceCryptoProvider and DefaultComplianceAuditor
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { p256 } from '@noble/curves/nist.js';
|
|
2
|
+
import { gcm } from '@noble/ciphers/aes.js';
|
|
3
|
+
import { randomBytes as nobleRandomBytes } from '@noble/ciphers/utils.js';
|
|
4
|
+
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
5
|
+
import { hmac } from '@noble/hashes/hmac.js';
|
|
6
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
7
|
+
import type { Logger } from '@bananalink-sdk/logger';
|
|
8
|
+
import type { CryptoProvider, CryptoKeyLike, ProviderKeyPair } from '../../types/crypto-provider';
|
|
9
|
+
import { registerCryptoProvider } from './registry';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Noble library implementation of CryptoKeyLike
|
|
13
|
+
*/
|
|
14
|
+
class NobleKey implements CryptoKeyLike {
|
|
15
|
+
constructor(
|
|
16
|
+
public readonly data: Uint8Array,
|
|
17
|
+
public readonly type: 'public' | 'private' | 'secret',
|
|
18
|
+
public readonly algorithm: string,
|
|
19
|
+
public readonly extractable: boolean = true,
|
|
20
|
+
public readonly usages: readonly string[] = []
|
|
21
|
+
) {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Noble cryptography implementation of CryptoProvider
|
|
26
|
+
* Pure JavaScript implementation that works in all environments
|
|
27
|
+
*/
|
|
28
|
+
export class NobleCryptoProvider implements CryptoProvider {
|
|
29
|
+
public readonly name = 'Noble';
|
|
30
|
+
public readonly isAvailable = true; // Noble libraries work everywhere
|
|
31
|
+
private readonly logger?: Logger;
|
|
32
|
+
|
|
33
|
+
constructor(logger?: Logger) {
|
|
34
|
+
this.logger = logger?.child({ component: 'NobleCryptoProvider' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate ECDH P-256 key pair
|
|
39
|
+
*/
|
|
40
|
+
generateKeyPair(): Promise<ProviderKeyPair> {
|
|
41
|
+
this.logger?.debug('Generating ECDH P-256 key pair using Noble');
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const privateKeyBytes = p256.utils.randomSecretKey();
|
|
45
|
+
// Generate uncompressed public key to match WebCrypto format (65 bytes)
|
|
46
|
+
const publicKeyUncompressed = p256.getPublicKey(privateKeyBytes, false); // false = uncompressed
|
|
47
|
+
|
|
48
|
+
this.logger?.debug('Key pair generation completed', {
|
|
49
|
+
publicKeyLength: publicKeyUncompressed.length
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return Promise.resolve({
|
|
53
|
+
privateKey: new NobleKey(privateKeyBytes, 'private', 'ECDH-P256', true, ['deriveKey']),
|
|
54
|
+
publicKey: new NobleKey(publicKeyUncompressed, 'public', 'ECDH-P256', true, []),
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
this.logger?.error('Key pair generation failed', {
|
|
58
|
+
error: {
|
|
59
|
+
message: error instanceof Error ? error.message : String(error),
|
|
60
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Export public key to ArrayBuffer (raw format)
|
|
69
|
+
*/
|
|
70
|
+
exportPublicKey(publicKey: CryptoKeyLike): Promise<ArrayBuffer> {
|
|
71
|
+
const nobleKey = publicKey as NobleKey;
|
|
72
|
+
if (nobleKey.type !== 'public') {
|
|
73
|
+
throw new Error('Expected public key');
|
|
74
|
+
}
|
|
75
|
+
return Promise.resolve(nobleKey.data.buffer.slice(nobleKey.data.byteOffset, nobleKey.data.byteOffset + nobleKey.data.byteLength));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Export private key to ArrayBuffer (raw format)
|
|
80
|
+
*/
|
|
81
|
+
exportPrivateKey(privateKey: CryptoKeyLike): Promise<ArrayBuffer> {
|
|
82
|
+
const nobleKey = privateKey as NobleKey;
|
|
83
|
+
if (nobleKey.type !== 'private') {
|
|
84
|
+
throw new Error('Expected private key');
|
|
85
|
+
}
|
|
86
|
+
return Promise.resolve(nobleKey.data.buffer.slice(nobleKey.data.byteOffset, nobleKey.data.byteOffset + nobleKey.data.byteLength));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Import public key from ArrayBuffer (raw format)
|
|
91
|
+
*/
|
|
92
|
+
importPublicKey(keyData: ArrayBuffer): Promise<CryptoKeyLike> {
|
|
93
|
+
const keyBytes = new Uint8Array(keyData);
|
|
94
|
+
|
|
95
|
+
// Debug logging
|
|
96
|
+
this.logger?.debug('importPublicKey called', {
|
|
97
|
+
keyLength: keyBytes.length,
|
|
98
|
+
keyBytesFirst20: Array.from(keyBytes.slice(0, 20)).map(b => b.toString(16).padStart(2, '0')).join(' '),
|
|
99
|
+
keyBytesHex: Array.from(keyBytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Validate public key using Noble's validation
|
|
103
|
+
try {
|
|
104
|
+
let processedKeyBytes: Uint8Array;
|
|
105
|
+
|
|
106
|
+
// Handle different key formats
|
|
107
|
+
if (keyBytes.length === 65) {
|
|
108
|
+
// Already in uncompressed format (0x04 + 32 + 32)
|
|
109
|
+
this.logger?.debug('Processing 65-byte key', {
|
|
110
|
+
firstByte: `0x${keyBytes[0].toString(16)}`
|
|
111
|
+
});
|
|
112
|
+
if (keyBytes[0] !== 0x04) {
|
|
113
|
+
throw new Error(`Expected uncompressed key (0x04 prefix), got 0x${keyBytes[0].toString(16)}`);
|
|
114
|
+
}
|
|
115
|
+
processedKeyBytes = keyBytes;
|
|
116
|
+
} else if (keyBytes.length === 64) {
|
|
117
|
+
// Raw format (32 + 32) - needs 0x04 prefix for Noble
|
|
118
|
+
// This is the WebCrypto export format
|
|
119
|
+
this.logger?.debug('Processing 64-byte key', {
|
|
120
|
+
action: 'adding 0x04 prefix'
|
|
121
|
+
});
|
|
122
|
+
processedKeyBytes = new Uint8Array(65);
|
|
123
|
+
processedKeyBytes[0] = 0x04; // Uncompressed point indicator
|
|
124
|
+
processedKeyBytes.set(keyBytes, 1); // Copy the 64 bytes after the prefix
|
|
125
|
+
} else {
|
|
126
|
+
throw new Error(`Invalid key length: expected 64 or 65 bytes, got ${keyBytes.length} bytes`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.logger?.debug('Key processing completed', {
|
|
130
|
+
processedLength: processedKeyBytes.length,
|
|
131
|
+
processedFirst20: Array.from(processedKeyBytes.slice(0, 20)).map(b => b.toString(16).padStart(2, '0')).join(' ')
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Validate using Noble's validation
|
|
135
|
+
const isValid = p256.utils.isValidPublicKey(processedKeyBytes, false); // false = allow uncompressed
|
|
136
|
+
this.logger?.debug('Noble key validation completed', {
|
|
137
|
+
isValid
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!isValid) {
|
|
141
|
+
// Try additional debugging
|
|
142
|
+
this.logger?.debug('Key validation failed, trying alternative approaches', {
|
|
143
|
+
originalValidation: false
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Try with compressed format check
|
|
147
|
+
const isValidCompressed = p256.utils.isValidPublicKey(processedKeyBytes, true);
|
|
148
|
+
this.logger?.debug('Alternative validation attempted', {
|
|
149
|
+
compressedFormatValid: isValidCompressed
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
throw new Error('Key failed Noble validation');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return Promise.resolve(new NobleKey(processedKeyBytes, 'public', 'ECDH-P256', true, []));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
this.logger?.error('Public key import failed', {
|
|
158
|
+
error: {
|
|
159
|
+
message: error instanceof Error ? error.message : String(error),
|
|
160
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
throw new Error(`Invalid P-256 public key: ${String(error)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Import private key from ArrayBuffer (raw format)
|
|
169
|
+
*/
|
|
170
|
+
importPrivateKey(keyData: ArrayBuffer): Promise<CryptoKeyLike> {
|
|
171
|
+
const keyBytes = new Uint8Array(keyData);
|
|
172
|
+
|
|
173
|
+
// Validate private key (Noble will throw if invalid)
|
|
174
|
+
try {
|
|
175
|
+
if (!p256.utils.isValidSecretKey(keyBytes)) {
|
|
176
|
+
throw new Error('Invalid secret key');
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw new Error(`Invalid P-256 private key: ${String(error)}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return Promise.resolve(new NobleKey(keyBytes, 'private', 'ECDH-P256', true, ['deriveKey']));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Derive shared secret from ECDH key agreement
|
|
187
|
+
*/
|
|
188
|
+
deriveSharedSecret(privateKey: CryptoKeyLike, publicKey: CryptoKeyLike): Promise<CryptoKeyLike> {
|
|
189
|
+
this.logger?.debug('Deriving shared secret using ECDH with Noble');
|
|
190
|
+
|
|
191
|
+
const privKey = privateKey as NobleKey;
|
|
192
|
+
const pubKey = publicKey as NobleKey;
|
|
193
|
+
|
|
194
|
+
if (privKey.type !== 'private') {
|
|
195
|
+
const error = new Error('Expected private key');
|
|
196
|
+
this.logger?.error('Shared secret derivation failed - invalid private key type', {
|
|
197
|
+
actualType: privKey.type
|
|
198
|
+
});
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
if (pubKey.type !== 'public') {
|
|
202
|
+
const error = new Error('Expected public key');
|
|
203
|
+
this.logger?.error('Shared secret derivation failed - invalid public key type', {
|
|
204
|
+
actualType: pubKey.type
|
|
205
|
+
});
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
// Perform ECDH key agreement - return x-coordinate only to match WebCrypto
|
|
211
|
+
const sharedSecret = p256.getSharedSecret(privKey.data, pubKey.data, false); // uncompressed
|
|
212
|
+
// Extract only the x-coordinate (32 bytes) to match WebCrypto behavior
|
|
213
|
+
const xCoordinate = sharedSecret.slice(1, 33); // Skip 0x04 prefix, take next 32 bytes
|
|
214
|
+
|
|
215
|
+
this.logger?.debug('Shared secret derivation completed', {
|
|
216
|
+
sharedSecretLength: xCoordinate.length
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return Promise.resolve(new NobleKey(xCoordinate, 'secret', 'AES-GCM', true, ['encrypt', 'decrypt']));
|
|
220
|
+
} catch (error) {
|
|
221
|
+
this.logger?.error('Shared secret derivation failed', {
|
|
222
|
+
error: {
|
|
223
|
+
message: error instanceof Error ? error.message : String(error),
|
|
224
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Derive AES-GCM key from shared secret using HKDF
|
|
233
|
+
*/
|
|
234
|
+
deriveEncryptionKey(sharedSecret: CryptoKeyLike, salt: ArrayBuffer, info: ArrayBuffer): Promise<CryptoKeyLike> {
|
|
235
|
+
const secretKey = sharedSecret as NobleKey;
|
|
236
|
+
|
|
237
|
+
if (secretKey.type !== 'secret') {
|
|
238
|
+
throw new Error('Expected secret key');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Perform HKDF with SHA-256
|
|
242
|
+
const derivedKey = hkdf(sha256, secretKey.data, new Uint8Array(salt), new Uint8Array(info), 32); // 32 bytes = 256 bits
|
|
243
|
+
|
|
244
|
+
return Promise.resolve(new NobleKey(derivedKey, 'secret', 'AES-GCM', true, ['encrypt', 'decrypt']));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Generate random bytes
|
|
249
|
+
*/
|
|
250
|
+
randomBytes(length: number): ArrayBuffer {
|
|
251
|
+
const bytes = nobleRandomBytes(length);
|
|
252
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Encrypt data using AES-GCM
|
|
257
|
+
*/
|
|
258
|
+
encrypt(key: CryptoKeyLike, data: ArrayBuffer, iv: ArrayBuffer): Promise<ArrayBuffer> {
|
|
259
|
+
this.logger?.debug('Encrypting data with AES-GCM using Noble', {
|
|
260
|
+
dataSize: data.byteLength,
|
|
261
|
+
ivSize: iv.byteLength
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const secretKey = key as NobleKey;
|
|
265
|
+
|
|
266
|
+
if (secretKey.type !== 'secret') {
|
|
267
|
+
const error = new Error('Expected secret key');
|
|
268
|
+
this.logger?.error('Encryption failed - invalid key type', {
|
|
269
|
+
actualType: secretKey.type
|
|
270
|
+
});
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const aesGcm = gcm(secretKey.data, new Uint8Array(iv));
|
|
276
|
+
const ciphertext = aesGcm.encrypt(new Uint8Array(data));
|
|
277
|
+
|
|
278
|
+
this.logger?.debug('Encryption completed', {
|
|
279
|
+
ciphertextSize: ciphertext.byteLength
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return Promise.resolve(ciphertext.buffer.slice(ciphertext.byteOffset, ciphertext.byteOffset + ciphertext.byteLength));
|
|
283
|
+
} catch (error) {
|
|
284
|
+
this.logger?.error('Encryption failed', {
|
|
285
|
+
error: {
|
|
286
|
+
message: error instanceof Error ? error.message : String(error),
|
|
287
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Decrypt data using AES-GCM
|
|
296
|
+
*/
|
|
297
|
+
decrypt(key: CryptoKeyLike, data: ArrayBuffer, iv: ArrayBuffer): Promise<ArrayBuffer> {
|
|
298
|
+
this.logger?.debug('Decrypting data with AES-GCM using Noble', {
|
|
299
|
+
dataSize: data.byteLength,
|
|
300
|
+
ivSize: iv.byteLength
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const secretKey = key as NobleKey;
|
|
304
|
+
|
|
305
|
+
if (secretKey.type !== 'secret') {
|
|
306
|
+
const error = new Error('Expected secret key');
|
|
307
|
+
this.logger?.error('Decryption failed - invalid key type', {
|
|
308
|
+
actualType: secretKey.type
|
|
309
|
+
});
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const aesGcm = gcm(secretKey.data, new Uint8Array(iv));
|
|
315
|
+
const plaintext = aesGcm.decrypt(new Uint8Array(data));
|
|
316
|
+
|
|
317
|
+
this.logger?.debug('Decryption completed', {
|
|
318
|
+
plaintextSize: plaintext.byteLength
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return Promise.resolve(plaintext.buffer.slice(plaintext.byteOffset, plaintext.byteOffset + plaintext.byteLength));
|
|
322
|
+
} catch (error) {
|
|
323
|
+
this.logger?.error('Decryption failed', {
|
|
324
|
+
error: {
|
|
325
|
+
message: error instanceof Error ? error.message : String(error),
|
|
326
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Generate HMAC-SHA256
|
|
335
|
+
*/
|
|
336
|
+
generateHMAC(key: CryptoKeyLike, data: ArrayBuffer): Promise<ArrayBuffer> {
|
|
337
|
+
const secretKey = key as NobleKey;
|
|
338
|
+
|
|
339
|
+
if (secretKey.type !== 'secret') {
|
|
340
|
+
throw new Error('Expected secret key');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const mac = hmac(sha256, secretKey.data, new Uint8Array(data));
|
|
344
|
+
return Promise.resolve(mac.buffer.slice(mac.byteOffset, mac.byteOffset + mac.byteLength));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Verify HMAC-SHA256
|
|
349
|
+
*/
|
|
350
|
+
verifyHMAC(key: CryptoKeyLike, data: ArrayBuffer, mac: ArrayBuffer): Promise<boolean> {
|
|
351
|
+
const secretKey = key as NobleKey;
|
|
352
|
+
|
|
353
|
+
if (secretKey.type !== 'secret') {
|
|
354
|
+
throw new Error('Expected secret key');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const expectedMac = hmac(sha256, secretKey.data, new Uint8Array(data));
|
|
359
|
+
const providedMac = new Uint8Array(mac);
|
|
360
|
+
|
|
361
|
+
// Constant-time comparison
|
|
362
|
+
if (expectedMac.length !== providedMac.length) {
|
|
363
|
+
return Promise.resolve(false);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let result = 0;
|
|
367
|
+
for (let i = 0; i < expectedMac.length; i++) {
|
|
368
|
+
result |= expectedMac[i] ^ providedMac[i];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return Promise.resolve(result === 0);
|
|
372
|
+
} catch {
|
|
373
|
+
return Promise.resolve(false);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Self-register Noble provider on import
|
|
380
|
+
* This allows the provider to be available when explicitly imported
|
|
381
|
+
*/
|
|
382
|
+
registerCryptoProvider('noble', (logger) => new NobleCryptoProvider(logger));
|
|
383
|
+
|
|
384
|
+
// TypeScript module augmentation to track this provider is available
|
|
385
|
+
declare global {
|
|
386
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
387
|
+
namespace BananaLink {
|
|
388
|
+
interface RegisteredCryptoProviders {
|
|
389
|
+
noble: true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|