@optimystic/quereus-plugin-crypto 0.2.0

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/src/digest.ts ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Digest Function for Quereus
3
+ *
4
+ * Computes the hash of all arguments combined.
5
+ * Uses SHA-256 from @noble/hashes for portable implementation.
6
+ * Compatible with React Native and all JS environments.
7
+ */
8
+
9
+ import { sha256 } from '@noble/hashes/sha2';
10
+ import { sha512 } from '@noble/hashes/sha2';
11
+ import { blake3 } from '@noble/hashes/blake3';
12
+ import { concatBytes, utf8ToBytes } from '@noble/hashes/utils';
13
+
14
+ /**
15
+ * Hash algorithm options
16
+ */
17
+ export type HashAlgorithm = 'sha256' | 'sha512' | 'blake3';
18
+
19
+ /**
20
+ * Input type for digest function - can be string, Uint8Array, or number
21
+ */
22
+ export type DigestInput = string | Uint8Array | number | boolean | null | undefined;
23
+
24
+ /**
25
+ * Options for the digest function
26
+ */
27
+ export interface DigestOptions {
28
+ /** Hash algorithm to use (default: sha256) */
29
+ algorithm?: HashAlgorithm;
30
+ /** Output format (default: uint8array) */
31
+ output?: 'uint8array' | 'hex';
32
+ }
33
+
34
+ /**
35
+ * Convert various input types to Uint8Array for hashing
36
+ */
37
+ function inputToBytes(input: DigestInput): Uint8Array {
38
+ if (input === null || input === undefined) {
39
+ return new Uint8Array(0);
40
+ }
41
+
42
+ if (typeof input === 'string') {
43
+ return utf8ToBytes(input);
44
+ }
45
+
46
+ if (input instanceof Uint8Array) {
47
+ return input;
48
+ }
49
+
50
+ if (typeof input === 'number') {
51
+ // Convert number to 8-byte big-endian representation
52
+ const buffer = new ArrayBuffer(8);
53
+ const view = new DataView(buffer);
54
+ view.setFloat64(0, input, false); // big-endian
55
+ return new Uint8Array(buffer);
56
+ }
57
+
58
+ if (typeof input === 'boolean') {
59
+ return new Uint8Array([input ? 1 : 0]);
60
+ }
61
+
62
+ // Fallback: convert to string then to bytes
63
+ return utf8ToBytes(String(input));
64
+ }
65
+
66
+ /**
67
+ * Get hash function based on algorithm
68
+ */
69
+ function getHashFunction(algorithm: HashAlgorithm): (data: Uint8Array) => Uint8Array {
70
+ switch (algorithm) {
71
+ case 'sha256':
72
+ return sha256;
73
+ case 'sha512':
74
+ return sha512;
75
+ case 'blake3':
76
+ return blake3;
77
+ default:
78
+ throw new Error(`Unsupported hash algorithm: ${algorithm}`);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Computes the hash of all arguments
84
+ *
85
+ * @param {...DigestInput} args - Variable number of arguments to hash
86
+ * @returns {Uint8Array} The computed hash as a Uint8Array
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * // Hash a string
91
+ * const hash1 = Digest('hello world');
92
+ *
93
+ * // Hash multiple arguments
94
+ * const hash2 = Digest('user:', 123, 'session');
95
+ *
96
+ * // Hash with specific algorithm
97
+ * const hash3 = Digest.withOptions({ algorithm: 'sha512' })('data1', 'data2');
98
+ *
99
+ * // Get hex output
100
+ * const hexHash = Digest.withOptions({ output: 'hex' })('hello');
101
+ * ```
102
+ */
103
+ export function Digest(...args: DigestInput[]): Uint8Array {
104
+ return DigestWithOptions({ algorithm: 'sha256', output: 'uint8array' }, ...args) as Uint8Array;
105
+ }
106
+
107
+ /**
108
+ * Digest function with custom options
109
+ */
110
+ export function DigestWithOptions(options: DigestOptions, ...args: DigestInput[]): Uint8Array | string {
111
+ const algorithm = options.algorithm || 'sha256';
112
+ const output = options.output || 'uint8array';
113
+
114
+ // Convert all arguments to bytes and concatenate
115
+ const byteArrays = args.map(inputToBytes);
116
+ const combined = concatBytes(...byteArrays);
117
+
118
+ // Hash the combined data
119
+ const hashFunction = getHashFunction(algorithm);
120
+ const hash = hashFunction(combined);
121
+
122
+ // Return in requested format
123
+ if (output === 'hex') {
124
+ return Array.from(hash)
125
+ .map(b => b.toString(16).padStart(2, '0'))
126
+ .join('');
127
+ }
128
+
129
+ return hash;
130
+ }
131
+
132
+ /**
133
+ * Create a digest function with preset options
134
+ */
135
+ Digest.withOptions = (options: DigestOptions) => {
136
+ return (...args: DigestInput[]) => DigestWithOptions(options, ...args);
137
+ };
138
+
139
+ /**
140
+ * Convenience functions for specific algorithms
141
+ */
142
+ Digest.sha256 = (...args: DigestInput[]): Uint8Array => {
143
+ return DigestWithOptions({ algorithm: 'sha256' }, ...args) as Uint8Array;
144
+ };
145
+
146
+ Digest.sha512 = (...args: DigestInput[]): Uint8Array => {
147
+ return DigestWithOptions({ algorithm: 'sha512' }, ...args) as Uint8Array;
148
+ };
149
+
150
+ Digest.blake3 = (...args: DigestInput[]): Uint8Array => {
151
+ return DigestWithOptions({ algorithm: 'blake3' }, ...args) as Uint8Array;
152
+ };
153
+
154
+ /**
155
+ * Hex output variants
156
+ */
157
+ Digest.hex = (...args: DigestInput[]): string => {
158
+ return DigestWithOptions({ algorithm: 'sha256', output: 'hex' }, ...args) as string;
159
+ };
160
+
161
+ Digest.sha256Hex = (...args: DigestInput[]): string => {
162
+ return DigestWithOptions({ algorithm: 'sha256', output: 'hex' }, ...args) as string;
163
+ };
164
+
165
+ Digest.sha512Hex = (...args: DigestInput[]): string => {
166
+ return DigestWithOptions({ algorithm: 'sha512', output: 'hex' }, ...args) as string;
167
+ };
168
+
169
+ Digest.blake3Hex = (...args: DigestInput[]): string => {
170
+ return DigestWithOptions({ algorithm: 'blake3', output: 'hex' }, ...args) as string;
171
+ };
172
+
173
+ export default Digest;
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Quereus Crypto Functions Plugin
3
+ *
4
+ * Provides cryptographic functions for SQL queries and ES modules:
5
+ * - digest: Hash functions (SHA-256, SHA-512, BLAKE3) with base64url encoding
6
+ * - sign: ECC signature generation (secp256k1, P-256, Ed25519)
7
+ * - verify: ECC signature verification
8
+ * - hashMod: Hash with modulo for fixed-size outputs
9
+ * - randomBytes: Generate cryptographically secure random bytes
10
+ *
11
+ * All functions use base64url encoding by default for SQL compatibility.
12
+ */
13
+
14
+ // Export idiomatic lowercase functions (primary API)
15
+ export {
16
+ digest,
17
+ sign,
18
+ verify,
19
+ hashMod,
20
+ randomBytes,
21
+ generatePrivateKey,
22
+ getPublicKey,
23
+ type HashAlgorithm,
24
+ type CurveType,
25
+ type Encoding,
26
+ } from './crypto.js';
27
+
28
+ // Legacy exports for backwards compatibility (if needed)
29
+ export { Digest, type DigestInput, type DigestOptions } from './digest.js';
30
+ export { Sign, type PrivateKeyInput, type SignOptions } from './sign.js';
31
+ export { SignatureValid, type BytesInput, type VerifyOptions } from './signature-valid.js';
32
+
33
+
package/src/plugin.ts ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Quereus Plugin Entry Point for Crypto Functions
3
+ *
4
+ * This module provides the plugin registration following Quereus 0.4.5 format.
5
+ * All metadata is in package.json - no manifest export needed.
6
+ */
7
+
8
+ import type { Database, FunctionFlags, SqlValue } from '@quereus/quereus';
9
+ import { digest, sign, verify, hashMod, randomBytes } from './crypto.js';
10
+
11
+ /**
12
+ * Plugin registration function
13
+ * This is called by Quereus when the plugin is loaded
14
+ */
15
+ export default function register(_db: Database, _config: Record<string, SqlValue> = {}) {
16
+ // Register crypto functions with Quereus
17
+ const functions = [
18
+ {
19
+ schema: {
20
+ name: 'digest',
21
+ numArgs: -1, // Variable arguments: data, algorithm?, inputEncoding?, outputEncoding?
22
+ flags: 1 as FunctionFlags, // UTF8
23
+ returnType: { typeClass: 'scalar' as const, sqlType: 'TEXT' },
24
+ implementation: (...args: SqlValue[]) => {
25
+ const [data, algorithm = 'sha256', inputEncoding = 'base64url', outputEncoding = 'base64url'] = args;
26
+ return digest(data as string, algorithm as any, inputEncoding as any, outputEncoding as any);
27
+ },
28
+ },
29
+ },
30
+ {
31
+ schema: {
32
+ name: 'sign',
33
+ numArgs: -1, // Variable arguments: data, privateKey, curve?, inputEncoding?, keyEncoding?, outputEncoding?
34
+ flags: 1 as FunctionFlags,
35
+ returnType: { typeClass: 'scalar' as const, sqlType: 'TEXT' },
36
+ implementation: (...args: SqlValue[]) => {
37
+ const [data, privateKey, curve = 'secp256k1', inputEncoding = 'base64url', keyEncoding = 'base64url', outputEncoding = 'base64url'] = args;
38
+ return sign(data as string, privateKey as string, curve as any, inputEncoding as any, keyEncoding as any, outputEncoding as any);
39
+ },
40
+ },
41
+ },
42
+ {
43
+ schema: {
44
+ name: 'verify',
45
+ numArgs: -1, // Variable arguments: data, signature, publicKey, curve?, inputEncoding?, sigEncoding?, keyEncoding?
46
+ flags: 1 as FunctionFlags,
47
+ returnType: { typeClass: 'scalar' as const, sqlType: 'INTEGER' },
48
+ implementation: (...args: SqlValue[]) => {
49
+ const [data, signature, publicKey, curve = 'secp256k1', inputEncoding = 'base64url', sigEncoding = 'base64url', keyEncoding = 'base64url'] = args;
50
+ const result = verify(data as string, signature as string, publicKey as string, curve as any, inputEncoding as any, sigEncoding as any, keyEncoding as any);
51
+ return result ? 1 : 0;
52
+ },
53
+ },
54
+ },
55
+ {
56
+ schema: {
57
+ name: 'hash_mod',
58
+ numArgs: -1, // Variable arguments: data, bits, algorithm?, inputEncoding?
59
+ flags: 1 as FunctionFlags,
60
+ returnType: { typeClass: 'scalar' as const, sqlType: 'INTEGER' },
61
+ implementation: (...args: SqlValue[]) => {
62
+ const [data, bits, algorithm = 'sha256', inputEncoding = 'base64url'] = args;
63
+ return hashMod(data as string, bits as number, algorithm as any, inputEncoding as any);
64
+ },
65
+ },
66
+ },
67
+ {
68
+ schema: {
69
+ name: 'random_bytes',
70
+ numArgs: -1, // Variable arguments: bits?, encoding?
71
+ flags: 1 as FunctionFlags,
72
+ returnType: { typeClass: 'scalar' as const, sqlType: 'TEXT' },
73
+ implementation: (...args: SqlValue[]) => {
74
+ const [bits = 256, encoding = 'base64url'] = args;
75
+ return randomBytes(bits as number, encoding as any);
76
+ },
77
+ },
78
+ },
79
+ ];
80
+
81
+ return {
82
+ functions,
83
+ vtables: [],
84
+ collations: [],
85
+ };
86
+ }
87
+
package/src/sign.ts ADDED
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Sign Function for Quereus
3
+ *
4
+ * Returns the signature for the given payload using an ECC private key.
5
+ * Uses secp256k1 from @noble/curves for portable implementation.
6
+ * Compatible with React Native and all JS environments.
7
+ */
8
+
9
+ import { secp256k1 } from '@noble/curves/secp256k1';
10
+ import { p256 } from '@noble/curves/nist';
11
+ import { ed25519 } from '@noble/curves/ed25519';
12
+ import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils';
13
+
14
+ /**
15
+ * Supported elliptic curve types
16
+ */
17
+ export type CurveType = 'secp256k1' | 'p256' | 'ed25519';
18
+
19
+ /**
20
+ * Private key input - can be Uint8Array, hex string, or bigint
21
+ */
22
+ export type PrivateKeyInput = Uint8Array | string | bigint;
23
+
24
+ /**
25
+ * Digest input - can be Uint8Array or hex string
26
+ */
27
+ export type DigestInput = Uint8Array | string;
28
+
29
+ /**
30
+ * Signature output format options
31
+ */
32
+ export type SignatureFormat = 'uint8array' | 'hex' | 'compact' | 'der';
33
+
34
+ /**
35
+ * Options for the Sign function
36
+ */
37
+ export interface SignOptions {
38
+ /** Elliptic curve to use (default: secp256k1) */
39
+ curve?: CurveType;
40
+ /** Output format for signature (default: uint8array) */
41
+ format?: SignatureFormat;
42
+ /** Additional entropy for signatures (hedged signatures) */
43
+ extraEntropy?: boolean | Uint8Array;
44
+ /** Use low-S canonical signatures (default: true) */
45
+ lowS?: boolean;
46
+ }
47
+
48
+ /**
49
+ * Normalize private key input to Uint8Array
50
+ */
51
+ function normalizePrivateKey(privateKey: PrivateKeyInput): Uint8Array {
52
+ if (privateKey instanceof Uint8Array) {
53
+ return privateKey;
54
+ }
55
+
56
+ if (typeof privateKey === 'string') {
57
+ // Assume hex string
58
+ return hexToBytes(privateKey);
59
+ }
60
+
61
+ if (typeof privateKey === 'bigint') {
62
+ // Convert bigint to 32-byte array (for secp256k1/p256)
63
+ const hex = privateKey.toString(16).padStart(64, '0');
64
+ return hexToBytes(hex);
65
+ }
66
+
67
+ throw new Error('Invalid private key format');
68
+ }
69
+
70
+ /**
71
+ * Normalize digest input to Uint8Array
72
+ */
73
+ function normalizeDigest(digest: DigestInput): Uint8Array {
74
+ if (digest instanceof Uint8Array) {
75
+ return digest;
76
+ }
77
+
78
+ if (typeof digest === 'string') {
79
+ return hexToBytes(digest);
80
+ }
81
+
82
+ throw new Error('Invalid digest format');
83
+ }
84
+
85
+ /**
86
+ * Format signature based on requested format
87
+ */
88
+ function formatSignature(signature: any, format: SignatureFormat, curve: CurveType): Uint8Array | string {
89
+ switch (format) {
90
+ case 'uint8array':
91
+ if (curve === 'ed25519') {
92
+ return signature;
93
+ }
94
+ return signature.toCompactRawBytes();
95
+
96
+ case 'hex':
97
+ if (curve === 'ed25519') {
98
+ return bytesToHex(signature);
99
+ }
100
+ return signature.toCompactHex();
101
+
102
+ case 'compact':
103
+ if (curve === 'ed25519') {
104
+ return signature;
105
+ }
106
+ return signature.toCompactRawBytes();
107
+
108
+ case 'der':
109
+ if (curve === 'ed25519') {
110
+ throw new Error('DER format not supported for ed25519');
111
+ }
112
+ // Note: DER encoding would require additional implementation
113
+ // For now, fall back to compact
114
+ return signature.toCompactRawBytes();
115
+
116
+ default:
117
+ throw new Error(`Unsupported signature format: ${format}`);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Sign a digest using the specified private key and curve
123
+ *
124
+ * @param {DigestInput} digest - The digest/hash to sign
125
+ * @param {PrivateKeyInput} privateKey - The private key to use for signing
126
+ * @param {SignOptions} [options] - Optional signing parameters
127
+ * @returns {Uint8Array | string} The signature in the requested format
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * // Basic usage with secp256k1
132
+ * const digest = new Uint8Array(32).fill(1); // Your hash here
133
+ * const privateKey = 'a'.repeat(64); // Your private key hex
134
+ * const signature = Sign(digest, privateKey);
135
+ *
136
+ * // With specific curve and format
137
+ * const sig = Sign(digest, privateKey, {
138
+ * curve: 'p256',
139
+ * format: 'hex'
140
+ * });
141
+ *
142
+ * // With hedged signatures for extra security
143
+ * const hedgedSig = Sign(digest, privateKey, {
144
+ * extraEntropy: true
145
+ * });
146
+ * ```
147
+ */
148
+ export function Sign(
149
+ digest: DigestInput,
150
+ privateKey: PrivateKeyInput,
151
+ options: SignOptions = {}
152
+ ): Uint8Array | string {
153
+ const {
154
+ curve = 'secp256k1',
155
+ format = 'uint8array',
156
+ extraEntropy = false,
157
+ lowS = true,
158
+ } = options;
159
+
160
+ const normalizedDigest = normalizeDigest(digest);
161
+ const normalizedPrivateKey = normalizePrivateKey(privateKey);
162
+
163
+ let signature: any;
164
+
165
+ switch (curve) {
166
+ case 'secp256k1': {
167
+ const signOptions: any = { lowS };
168
+ if (extraEntropy) {
169
+ signOptions.extraEntropy = extraEntropy;
170
+ }
171
+ signature = secp256k1.sign(normalizedDigest, normalizedPrivateKey, signOptions);
172
+ break;
173
+ }
174
+
175
+ case 'p256': {
176
+ const signOptions: any = { lowS };
177
+ if (extraEntropy) {
178
+ signOptions.extraEntropy = extraEntropy;
179
+ }
180
+ signature = p256.sign(normalizedDigest, normalizedPrivateKey, signOptions);
181
+ break;
182
+ }
183
+
184
+ case 'ed25519': {
185
+ signature = ed25519.sign(normalizedDigest, normalizedPrivateKey);
186
+ break;
187
+ }
188
+
189
+ default:
190
+ throw new Error(`Unsupported curve: ${curve}`);
191
+ }
192
+
193
+ return formatSignature(signature, format, curve);
194
+ }
195
+
196
+ /**
197
+ * Convenience functions for specific curves
198
+ */
199
+ Sign.secp256k1 = (digest: DigestInput, privateKey: PrivateKeyInput, options: Omit<SignOptions, 'curve'> = {}) => {
200
+ return Sign(digest, privateKey, { ...options, curve: 'secp256k1' });
201
+ };
202
+
203
+ Sign.p256 = (digest: DigestInput, privateKey: PrivateKeyInput, options: Omit<SignOptions, 'curve'> = {}) => {
204
+ return Sign(digest, privateKey, { ...options, curve: 'p256' });
205
+ };
206
+
207
+ Sign.ed25519 = (digest: DigestInput, privateKey: PrivateKeyInput, options: Omit<SignOptions, 'curve'> = {}) => {
208
+ return Sign(digest, privateKey, { ...options, curve: 'ed25519' });
209
+ };
210
+
211
+ /**
212
+ * Generate a random private key for the specified curve
213
+ */
214
+ Sign.generatePrivateKey = (curve: CurveType = 'secp256k1'): Uint8Array => {
215
+ switch (curve) {
216
+ case 'secp256k1':
217
+ return secp256k1.utils.randomPrivateKey();
218
+ case 'p256':
219
+ return p256.utils.randomPrivateKey();
220
+ case 'ed25519':
221
+ return ed25519.utils.randomPrivateKey();
222
+ default:
223
+ throw new Error(`Unsupported curve: ${curve}`);
224
+ }
225
+ };
226
+
227
+ /**
228
+ * Get the public key for a given private key and curve
229
+ */
230
+ Sign.getPublicKey = (privateKey: PrivateKeyInput, curve: CurveType = 'secp256k1'): Uint8Array => {
231
+ const normalizedPrivateKey = normalizePrivateKey(privateKey);
232
+
233
+ switch (curve) {
234
+ case 'secp256k1':
235
+ return secp256k1.getPublicKey(normalizedPrivateKey);
236
+ case 'p256':
237
+ return p256.getPublicKey(normalizedPrivateKey);
238
+ case 'ed25519':
239
+ return ed25519.getPublicKey(normalizedPrivateKey);
240
+ default:
241
+ throw new Error(`Unsupported curve: ${curve}`);
242
+ }
243
+ };
244
+
245
+ export default Sign;