eyeprolog 1.5.7 → 1.5.9

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 CHANGED
@@ -61,6 +61,8 @@ Normal mode executes top-level `Conclusion :+ Premise` rules through the bundled
61
61
 
62
62
  Bundled `src/lib/` predicates autoload in files, CLI/API goals, and the REPL; use `--no-autoload` to require explicit imports, and explicitly import libraries that introduce operators before using their syntax. See [*The Art of EyeProlog*](the-art-of-eyeprolog.md) for the full semantics.
63
63
 
64
+ The Trealla/Scryer compatibility surface now also includes chars-based term I/O and Base64 helpers, rational-form conversion in `library(arithmetic)`, `sleep/1`, the shared `library(iso_ext)` control/term re-exports, focused `library(files)` and `library(os)` modules, and Scryer-compatible `library(crypto)`. The crypto module covers hashing/HMAC, HKDF, PBKDF2 password hashes, ChaCha20-Poly1305, Ed25519, X25519, and secp256k1 helpers; hashing/KDF/encryption and Ed25519/X25519 use Node's cryptographic backend, secure random bytes also use Web Crypto when available, and unsupported browser operations report `resource_error(crypto)`. Filesystem and OS side effects likewise require Node. Non-integral `number_to_rational/2` results are represented structurally as `rdiv(Numerator,Denominator)` until rational numbers become processor numeric values.
65
+
64
66
  ## Links
65
67
 
66
68
  - [The Art of EyeProlog](https://eyereasoner.github.io/eyeprolog/the-art-of-eyeprolog) — complete reference
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.7",
6
+ "version": "1.5.9",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -16,7 +16,7 @@ higher-level frontends.
16
16
  iterators and registers the normal-profile cleanup controls without making
17
17
  `solver.js` depend back on the language registry.
18
18
  4. **Language services** — `iso.js`, `iso-arithmetic.js`, `dcg.js`, `atts.js`,
19
- `standard-library.js`, and `src/lib/`.
19
+ `scryer-compat.js`, `library-host.js`, `standard-library.js`, and `src/lib/`.
20
20
  5. **Frontends/tools** — `execute.js`, `repl.js`, `cli.js`, `quads.js`,
21
21
  `explain.js`, and the playground worker.
22
22
 
@@ -0,0 +1,628 @@
1
+ // Host adapters for library(crypto).
2
+ //
3
+ // The public compatibility surface lives in src/lib/crypto.pl. Keep the
4
+ // runtime-specific cryptographic operations here so the Prolog module remains
5
+ // source-compatible with Scryer's library while Node's audited crypto backend
6
+ // performs the sensitive primitives.
7
+
8
+ import { isNode } from './platform.js';
9
+ import {
10
+ ATOM, COMPOUND, NUMBER, VAR, atom, compound, copyResolved, deref,
11
+ listFromItems, numberTerm, properListItems, unify,
12
+ } from './term.js';
13
+ import { PrologError } from './errors.js';
14
+
15
+ let nodeCrypto = null;
16
+ if (isNode) nodeCrypto = await import('node:crypto');
17
+
18
+ const HASH_ALGORITHMS = new Map([
19
+ ['ripemd160', 'ripemd160'],
20
+ ['sha256', 'sha256'],
21
+ ['sha384', 'sha384'],
22
+ ['sha512', 'sha512'],
23
+ ['sha512_256', 'sha512-256'],
24
+ ['sha3_224', 'sha3-224'],
25
+ ['sha3_256', 'sha3-256'],
26
+ ['sha3_384', 'sha3-384'],
27
+ ['sha3_512', 'sha3-512'],
28
+ ['blake2s256', 'blake2s256'],
29
+ ['blake2b512', 'blake2b512'],
30
+ ]);
31
+ const HMAC_ALGORITHMS = new Set(['sha256', 'sha384', 'sha512']);
32
+ const HKDF_ALGORITHMS = new Set(['sha256', 'sha384', 'sha512']);
33
+ const ED25519_PRIVATE_PREFIX = hexBuffer('302e020100300506032b657004220420');
34
+ const ED25519_PUBLIC_PREFIX = hexBuffer('302a300506032b6570032100');
35
+ const X25519_PRIVATE_PREFIX = hexBuffer('302e020100300506032b656e04220420');
36
+ const X25519_PUBLIC_PREFIX = hexBuffer('302a300506032b656e032100');
37
+ const SECP256K1_P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn;
38
+ const SECP256K1_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
39
+ const SECP256K1_GX = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n;
40
+ const SECP256K1_GY = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n;
41
+
42
+ function hexBuffer(text) {
43
+ if (typeof Buffer === 'undefined') return null;
44
+ return Buffer.from(text, 'hex');
45
+ }
46
+
47
+ function requireCrypto() {
48
+ if (!isNode || nodeCrypto == null || typeof Buffer === 'undefined') {
49
+ throw new PrologError('resource_error(crypto)');
50
+ }
51
+ return nodeCrypto;
52
+ }
53
+
54
+ function listItems(term, env) {
55
+ const value = deref(term, env);
56
+ if (value.type === VAR) throw new PrologError('instantiation_error');
57
+ const items = properListItems(value, env);
58
+ if (items == null) throw new PrologError('type_error(list)', copyResolved(value, env));
59
+ return items;
60
+ }
61
+
62
+ function characterListText(term, env, { octets = false, allowIntegers = false } = {}) {
63
+ const items = listItems(term, env);
64
+ let text = '';
65
+ for (const itemTerm of items) {
66
+ const item = deref(itemTerm, env);
67
+ if (item.type === VAR) throw new PrologError('instantiation_error');
68
+ if (allowIntegers && item.type === NUMBER && /^\d+$/.test(item.name)) {
69
+ const code = Number(item.name);
70
+ if (!Number.isInteger(code) || code < 0 || code > 255) {
71
+ throw new PrologError('type_error(byte)', copyResolved(item, env));
72
+ }
73
+ text += String.fromCodePoint(code);
74
+ continue;
75
+ }
76
+ if (item.type !== ATOM || Array.from(item.name).length !== 1) {
77
+ throw new PrologError('type_error(character)', copyResolved(item, env));
78
+ }
79
+ const code = item.name.codePointAt(0);
80
+ if (octets && code > 255) throw new PrologError('domain_error(octet_character)', copyResolved(item, env));
81
+ text += item.name;
82
+ }
83
+ return text;
84
+ }
85
+
86
+ function bytesFromList(term, env, expectedLength = null) {
87
+ const items = listItems(term, env);
88
+ const bytes = [];
89
+ for (const itemTerm of items) {
90
+ const item = deref(itemTerm, env);
91
+ if (item.type === VAR) throw new PrologError('instantiation_error');
92
+ if (item.type !== NUMBER || !/^\d+$/.test(item.name)) {
93
+ throw new PrologError('type_error(integer)', copyResolved(item, env));
94
+ }
95
+ const value = Number(item.name);
96
+ if (!Number.isInteger(value) || value < 0 || value > 255) {
97
+ throw new PrologError('type_error(byte)', copyResolved(item, env));
98
+ }
99
+ bytes.push(value);
100
+ }
101
+ if (expectedLength != null && bytes.length !== expectedLength) {
102
+ throw new PrologError('domain_error(byte_length)', copyResolved(term, env));
103
+ }
104
+ return typeof Buffer === 'undefined' ? Uint8Array.from(bytes) : Buffer.from(bytes);
105
+ }
106
+
107
+ function byteChars(term, env, expectedLength = null) {
108
+ const text = characterListText(term, env, { octets: true });
109
+ const bytes = Buffer.from(Array.from(text, (ch) => ch.codePointAt(0)));
110
+ if (expectedLength != null && bytes.length !== expectedLength) {
111
+ throw new PrologError('domain_error(byte_length)', copyResolved(term, env));
112
+ }
113
+ return bytes;
114
+ }
115
+
116
+ function charsTerm(text) {
117
+ return listFromItems(Array.from(String(text), atom));
118
+ }
119
+
120
+ function byteCharsTerm(bytes) {
121
+ return listFromItems(Array.from(bytes, (value) => atom(String.fromCodePoint(value))));
122
+ }
123
+
124
+ function byteListTerm(bytes) {
125
+ return listFromItems(Array.from(bytes, (value) => numberTerm(value)));
126
+ }
127
+
128
+ function optionsList(term, env) {
129
+ const options = listItems(term, env).map((item) => deref(item, env));
130
+ if (options.some((option) => option.type === VAR)) throw new PrologError('instantiation_error');
131
+ return options;
132
+ }
133
+
134
+ function findOption(options, name) {
135
+ return options.find((option) => option.type === COMPOUND && option.name === name && option.arity === 1) ?? null;
136
+ }
137
+
138
+ function atomOption(options, name, env, fallback, allowed, next = null, bindVariableToDefault = false) {
139
+ const option = findOption(options, name);
140
+ if (option == null) return fallback;
141
+ const value = deref(option.args[0], env);
142
+ if (value.type === VAR) {
143
+ if (!bindVariableToDefault) throw new PrologError('instantiation_error');
144
+ if (next != null && !unify(option.args[0], atom(fallback), next)) return null;
145
+ return fallback;
146
+ }
147
+ if (value.type !== ATOM) throw new PrologError('type_error(atom)', copyResolved(value, env));
148
+ if (allowed != null && !allowed.has(value.name)) {
149
+ throw new PrologError(`domain_error(${name})`, copyResolved(value, env));
150
+ }
151
+ return value.name;
152
+ }
153
+
154
+ function integerTermValue(term, env) {
155
+ const value = deref(term, env);
156
+ if (value.type === VAR) throw new PrologError('instantiation_error');
157
+ if (value.type !== NUMBER || !/^-?\d+$/.test(value.name)) {
158
+ throw new PrologError('type_error(integer)', copyResolved(value, env));
159
+ }
160
+ return BigInt(value.name);
161
+ }
162
+
163
+ function smallInteger(term, env, { min = null, max = null } = {}) {
164
+ const value = integerTermValue(term, env);
165
+ if (min != null && value < BigInt(min)) throw new PrologError('domain_error(not_less_than_zero)', copyResolved(term, env));
166
+ if (max != null && value > BigInt(max)) throw new PrologError('representation_error(max_integer)', copyResolved(term, env));
167
+ const number = Number(value);
168
+ if (!Number.isSafeInteger(number)) throw new PrologError('representation_error(max_integer)', copyResolved(term, env));
169
+ return number;
170
+ }
171
+
172
+ function encodingBytes(term, env, encoding) {
173
+ if (encoding === 'octet') {
174
+ const text = characterListText(term, env, { octets: true, allowIntegers: true });
175
+ return Buffer.from(Array.from(text, (ch) => ch.codePointAt(0)));
176
+ }
177
+ const text = characterListText(term, env);
178
+ return Buffer.from(text, 'utf8');
179
+ }
180
+
181
+ function flexibleBytes(term, env) {
182
+ const items = listItems(term, env);
183
+ if (items.every((itemTerm) => {
184
+ const item = deref(itemTerm, env);
185
+ return item.type === NUMBER && /^\d+$/.test(item.name) && Number(item.name) >= 0 && Number(item.name) <= 255;
186
+ })) return bytesFromList(term, env);
187
+ return Buffer.from(characterListText(term, env), 'utf8');
188
+ }
189
+
190
+ function hexText(bytes) {
191
+ return Array.from(bytes, (value) => Number(value).toString(16).padStart(2, '0')).join('');
192
+ }
193
+
194
+ function decodeHex(term, env) {
195
+ const text = characterListText(term, env);
196
+ if (text.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(text)) {
197
+ throw new PrologError('domain_error(hex_encoding)', copyResolved(term, env));
198
+ }
199
+ const values = [];
200
+ for (let index = 0; index < text.length; index += 2) values.push(Number.parseInt(text.slice(index, index + 2), 16));
201
+ return typeof Buffer === 'undefined' ? Uint8Array.from(values) : Buffer.from(values);
202
+ }
203
+
204
+ function bindResult(target, value, env) {
205
+ const next = env.clone();
206
+ return unify(target, value, next) ? next : null;
207
+ }
208
+
209
+ function* hexBytesBuiltin({ goal, env }) {
210
+ const hex = deref(goal.args[0], env);
211
+ if (hex.type !== VAR) {
212
+ const bytes = decodeHex(goal.args[0], env);
213
+ const next = bindResult(goal.args[1], byteListTerm(bytes), env);
214
+ if (next) yield next;
215
+ return;
216
+ }
217
+ const bytes = bytesFromList(goal.args[1], env);
218
+ const next = bindResult(goal.args[0], charsTerm(hexText(bytes)), env);
219
+ if (next) yield next;
220
+ }
221
+
222
+ function* randomBytesBuiltin({ goal, env }) {
223
+ const length = smallInteger(goal.args[0], env, { min: 0, max: 0x7fffffff });
224
+ let bytes;
225
+ if (isNode && nodeCrypto != null) bytes = nodeCrypto.randomBytes(length);
226
+ else if (globalThis.crypto?.getRandomValues != null) {
227
+ bytes = new Uint8Array(length);
228
+ // Web Crypto limits one getRandomValues() request to 65536 bytes.
229
+ for (let offset = 0; offset < bytes.length; offset += 65536) {
230
+ globalThis.crypto.getRandomValues(bytes.subarray(offset, Math.min(bytes.length, offset + 65536)));
231
+ }
232
+ } else {
233
+ throw new PrologError('resource_error(crypto)');
234
+ }
235
+ const next = bindResult(goal.args[1], byteListTerm(bytes), env);
236
+ if (next) yield next;
237
+ }
238
+
239
+ function* dataHashBuiltin({ goal, env }) {
240
+ const crypto = requireCrypto();
241
+ const options = optionsList(goal.args[2], env);
242
+ const next = env.clone();
243
+ const algorithm = atomOption(options, 'algorithm', env, 'sha256', new Set(HASH_ALGORITHMS.keys()), next, true);
244
+ if (algorithm == null) return;
245
+ const encoding = atomOption(options, 'encoding', env, 'utf8', new Set(['utf8', 'octet']));
246
+ const data = encodingBytes(goal.args[0], env, encoding);
247
+ const hmac = findOption(options, 'hmac');
248
+ let digest;
249
+ if (hmac != null) {
250
+ if (!HMAC_ALGORITHMS.has(algorithm)) {
251
+ throw new PrologError('domain_error(hmac_algorithm)', atom(algorithm));
252
+ }
253
+ const key = bytesFromList(hmac.args[0], env);
254
+ digest = crypto.createHmac(HASH_ALGORITHMS.get(algorithm), key).update(data).digest();
255
+ const hashValue = deref(goal.args[1], env);
256
+ if (hashValue.type !== VAR) {
257
+ const expected = decodeHex(goal.args[1], env);
258
+ if (expected.length !== digest.length || !crypto.timingSafeEqual(expected, digest)) return;
259
+ yield next;
260
+ return;
261
+ }
262
+ } else {
263
+ digest = crypto.createHash(HASH_ALGORITHMS.get(algorithm)).update(data).digest();
264
+ }
265
+ if (unify(goal.args[1], charsTerm(hexText(digest)), next)) yield next;
266
+ }
267
+
268
+ function* hkdfBuiltin({ goal, env }) {
269
+ const crypto = requireCrypto();
270
+ const options = optionsList(goal.args[3], env);
271
+ const next = env.clone();
272
+ const algorithm = atomOption(options, 'algorithm', env, 'sha256', HKDF_ALGORITHMS, next, true);
273
+ if (algorithm == null) return;
274
+ const encoding = atomOption(options, 'encoding', env, 'utf8', new Set(['utf8', 'octet']));
275
+ const length = smallInteger(goal.args[1], env, { min: 0, max: 0x7fffffff });
276
+ const data = encodingBytes(goal.args[0], env, encoding);
277
+ const saltOption = findOption(options, 'salt');
278
+ const salt = saltOption == null ? Buffer.alloc(0) : bytesFromList(saltOption.args[0], env);
279
+ const infoOption = findOption(options, 'info');
280
+ const info = infoOption == null ? Buffer.alloc(0) : flexibleBytes(infoOption.args[0], env);
281
+ let derived;
282
+ try { derived = Buffer.from(crypto.hkdfSync(HASH_ALGORITHMS.get(algorithm), data, salt, info, length)); }
283
+ catch { throw new PrologError('resource_error(crypto)'); }
284
+ if (unify(goal.args[2], byteListTerm(derived), next)) yield next;
285
+ }
286
+
287
+ function passwordBytes(term, env) {
288
+ return flexibleBytes(term, env);
289
+ }
290
+
291
+ function base64NoPadding(bytes) {
292
+ return Buffer.from(bytes).toString('base64').replace(/=+$/u, '');
293
+ }
294
+
295
+ function decodeBase64NoPadding(text) {
296
+ if (!/^[A-Za-z0-9+/]*$/u.test(text)) return null;
297
+ try { return Buffer.from(text, 'base64'); } catch { return null; }
298
+ }
299
+
300
+ function passwordHashString(password, salt, iterations, crypto) {
301
+ const hash = crypto.pbkdf2Sync(password, salt, iterations, 64, 'sha512');
302
+ return `$pbkdf2-sha512$t=${iterations}$${base64NoPadding(salt)}$${base64NoPadding(hash)}`;
303
+ }
304
+
305
+ function* passwordHash2Builtin({ goal, env }) {
306
+ const crypto = requireCrypto();
307
+ const password = passwordBytes(goal.args[0], env);
308
+ const hashValue = deref(goal.args[1], env);
309
+ if (hashValue.type === VAR) {
310
+ const salt = crypto.randomBytes(16);
311
+ const output = passwordHashString(password, salt, 2 ** 17, crypto);
312
+ const next = bindResult(goal.args[1], charsTerm(output), env);
313
+ if (next) yield next;
314
+ return;
315
+ }
316
+ const encoded = characterListText(goal.args[1], env);
317
+ const match = /^\$pbkdf2-sha512\$t=(\d+)\$([A-Za-z0-9+/]+)\$([A-Za-z0-9+/]+)$/u.exec(encoded);
318
+ if (!match) return;
319
+ const iterations = Number(match[1]);
320
+ if (!Number.isSafeInteger(iterations) || iterations <= 0) return;
321
+ const salt = decodeBase64NoPadding(match[2]);
322
+ const expected = decodeBase64NoPadding(match[3]);
323
+ if (salt == null || expected == null) return;
324
+ const actual = crypto.pbkdf2Sync(password, salt, iterations, expected.length, 'sha512');
325
+ if (actual.length === expected.length && crypto.timingSafeEqual(actual, expected)) yield env;
326
+ }
327
+
328
+ function* passwordHash3Builtin({ goal, env }) {
329
+ const crypto = requireCrypto();
330
+ const password = passwordBytes(goal.args[0], env);
331
+ const options = optionsList(goal.args[2], env);
332
+ const algorithmOption = findOption(options, 'algorithm');
333
+ if (algorithmOption != null) {
334
+ const algorithm = deref(algorithmOption.args[0], env);
335
+ if (algorithm.type === VAR) {
336
+ // Scryer's option/3 call unifies this variable with its only supported algorithm.
337
+ } else if (algorithm.type !== ATOM || algorithm.name !== 'pbkdf2-sha512') {
338
+ throw new PrologError('domain_error(password_hash_algorithm)', copyResolved(algorithm, env));
339
+ }
340
+ }
341
+ const costOption = findOption(options, 'cost');
342
+ const cost = costOption == null ? 17 : smallInteger(costOption.args[0], env, { min: 0, max: 30 });
343
+ const iterations = 2 ** cost;
344
+ const saltOption = findOption(options, 'salt');
345
+ const salt = saltOption == null ? crypto.randomBytes(16) : bytesFromList(saltOption.args[0], env);
346
+ const output = passwordHashString(password, salt, iterations, crypto);
347
+ const next = env.clone();
348
+ if (algorithmOption != null && deref(algorithmOption.args[0], env).type === VAR &&
349
+ !unify(algorithmOption.args[0], atom('pbkdf2-sha512'), next)) return;
350
+ if (unify(goal.args[1], charsTerm(output), next)) yield next;
351
+ }
352
+
353
+ function chachaInputs(goal, env, options, decrypt = false) {
354
+ const algorithm = deref(goal.args[1], env);
355
+ if (algorithm.type === VAR) throw new PrologError('instantiation_error');
356
+ if (algorithm.type !== ATOM) throw new PrologError('type_error(atom)', copyResolved(algorithm, env));
357
+ if (algorithm.name !== 'chacha20-poly1305') {
358
+ throw new PrologError('domain_error(chacha20-poly1305)', copyResolved(algorithm, env));
359
+ }
360
+ const key = bytesFromList(goal.args[2], env, 32);
361
+ const iv = bytesFromList(goal.args[3], env, 12);
362
+ const encoding = atomOption(options, 'encoding', env, 'utf8', new Set(['utf8', 'octet']));
363
+ const aadOption = findOption(options, 'aad');
364
+ const aad = aadOption == null ? Buffer.alloc(0) : encodingBytes(aadOption.args[0], env, encoding);
365
+ const tagOption = findOption(options, 'tag');
366
+ let tag = null;
367
+ if (decrypt) {
368
+ if (tagOption == null) tag = Buffer.alloc(0);
369
+ else tag = bytesFromList(tagOption.args[0], env);
370
+ if (tag.length !== 16) throw new PrologError('domain_error(authentication_tag)');
371
+ } else if (tagOption != null && deref(tagOption.args[0], env).type !== VAR) {
372
+ bytesFromList(tagOption.args[0], env, 16);
373
+ }
374
+ return { key, iv, encoding, aad, tagOption, tag };
375
+ }
376
+
377
+ function* encryptBuiltin({ goal, env }) {
378
+ const crypto = requireCrypto();
379
+ const options = optionsList(goal.args[5], env);
380
+ const { key, iv, encoding, aad, tagOption } = chachaInputs(goal, env, options, false);
381
+ const plain = encodingBytes(goal.args[0], env, encoding);
382
+ let cipher, ciphertext, tag;
383
+ try {
384
+ cipher = crypto.createCipheriv('chacha20-poly1305', key, iv, { authTagLength: 16 });
385
+ if (aad.length) cipher.setAAD(aad, { plaintextLength: plain.length });
386
+ ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]);
387
+ tag = cipher.getAuthTag();
388
+ } catch { throw new PrologError('resource_error(crypto)'); }
389
+ const next = env.clone();
390
+ if (tagOption != null && !unify(tagOption.args[0], byteListTerm(tag), next)) return;
391
+ if (unify(goal.args[4], byteCharsTerm(ciphertext), next)) yield next;
392
+ }
393
+
394
+ function* decryptBuiltin({ goal, env }) {
395
+ const crypto = requireCrypto();
396
+ const options = optionsList(goal.args[5], env);
397
+ const { key, iv, encoding, aad, tag } = chachaInputs(goal, env, options, true);
398
+ const ciphertext = byteChars(goal.args[0], env);
399
+ let plain;
400
+ try {
401
+ const decipher = crypto.createDecipheriv('chacha20-poly1305', key, iv, { authTagLength: 16 });
402
+ if (aad.length) decipher.setAAD(aad, { plaintextLength: ciphertext.length });
403
+ decipher.setAuthTag(tag);
404
+ plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
405
+ } catch { return; }
406
+ let result;
407
+ if (encoding === 'octet') result = byteCharsTerm(plain);
408
+ else {
409
+ let text;
410
+ try { text = new TextDecoder('utf-8', { fatal: true }).decode(plain); }
411
+ catch { throw new PrologError('representation_error(character_code)'); }
412
+ result = charsTerm(text);
413
+ }
414
+ const next = bindResult(goal.args[4], result, env);
415
+ if (next) yield next;
416
+ }
417
+
418
+ function ed25519PrivateKey(seed) {
419
+ const crypto = requireCrypto();
420
+ return crypto.createPrivateKey({ key: Buffer.concat([ED25519_PRIVATE_PREFIX, seed]), format: 'der', type: 'pkcs8' });
421
+ }
422
+
423
+ function ed25519PublicFromSeed(seed) {
424
+ const crypto = requireCrypto();
425
+ const der = crypto.createPublicKey(ed25519PrivateKey(seed)).export({ format: 'der', type: 'spki' });
426
+ return Buffer.from(der).subarray(-32);
427
+ }
428
+
429
+ function ed25519Pair(seed) {
430
+ const publicKey = ed25519PublicFromSeed(seed);
431
+ return Buffer.concat([
432
+ Buffer.from([0x30, 81, 2, 1, 1, 0x30, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]),
433
+ seed,
434
+ Buffer.from([0x81, 33, 0]),
435
+ publicKey,
436
+ ]);
437
+ }
438
+
439
+ function* edSeedPairBuiltin({ goal, env }) {
440
+ requireCrypto();
441
+ const seed = bytesFromList(goal.args[0], env, 32);
442
+ const next = bindResult(goal.args[1], byteCharsTerm(ed25519Pair(seed)), env);
443
+ if (next) yield next;
444
+ }
445
+
446
+ function* edNewPairBuiltin({ goal, env }) {
447
+ const crypto = requireCrypto();
448
+ const next = bindResult(goal.args[0], byteCharsTerm(ed25519Pair(crypto.randomBytes(32))), env);
449
+ if (next) yield next;
450
+ }
451
+
452
+ function* edPublicBuiltin({ goal, env }) {
453
+ const pair = byteChars(goal.args[0], env);
454
+ if (pair.length < 32) throw new PrologError('domain_error(ed25519_keypair)', copyResolved(goal.args[0], env));
455
+ const next = bindResult(goal.args[1], byteCharsTerm(pair.subarray(pair.length - 32)), env);
456
+ if (next) yield next;
457
+ }
458
+
459
+ function dataEncodingOption(options, env) {
460
+ return atomOption(options, 'encoding', env, 'utf8', new Set(['utf8', 'octet']));
461
+ }
462
+
463
+ function* edSignBuiltin({ goal, env }) {
464
+ const crypto = requireCrypto();
465
+ const pair = byteChars(goal.args[0], env);
466
+ if (pair.length < 48) throw new PrologError('domain_error(ed25519_keypair)', copyResolved(goal.args[0], env));
467
+ const seed = pair.subarray(16, 48);
468
+ const options = optionsList(goal.args[3], env);
469
+ const data = encodingBytes(goal.args[1], env, dataEncodingOption(options, env));
470
+ let signature;
471
+ try { signature = crypto.sign(null, data, ed25519PrivateKey(seed)); }
472
+ catch { throw new PrologError('resource_error(crypto)'); }
473
+ const next = bindResult(goal.args[2], charsTerm(hexText(signature)), env);
474
+ if (next) yield next;
475
+ }
476
+
477
+ function* edVerifyBuiltin({ goal, env }) {
478
+ const crypto = requireCrypto();
479
+ const publicKey = byteChars(goal.args[0], env, 32);
480
+ const options = optionsList(goal.args[3], env);
481
+ const data = encodingBytes(goal.args[1], env, dataEncodingOption(options, env));
482
+ const signature = decodeHex(goal.args[2], env);
483
+ if (signature.length !== 64) return;
484
+ let ok;
485
+ try {
486
+ const key = crypto.createPublicKey({ key: Buffer.concat([ED25519_PUBLIC_PREFIX, publicKey]), format: 'der', type: 'spki' });
487
+ ok = crypto.verify(null, data, key, signature);
488
+ } catch { throw new PrologError('resource_error(crypto)'); }
489
+ if (ok) yield env;
490
+ }
491
+
492
+ function* curve25519GeneratorBuiltin({ goal, env }) {
493
+ const bytes = new Uint8Array(32); bytes[0] = 9;
494
+ const next = bindResult(goal.args[0], byteCharsTerm(bytes), env);
495
+ if (next) yield next;
496
+ }
497
+
498
+ function scalar25519Bytes(term, env) {
499
+ const value = deref(term, env);
500
+ if (value.type === NUMBER && /^\d+$/.test(value.name)) {
501
+ let scalar = BigInt(value.name);
502
+ if (scalar < 0n || scalar >= (1n << 256n)) throw new PrologError('domain_error(curve25519_scalar)', copyResolved(value, env));
503
+ const bytes = Buffer.alloc(32);
504
+ for (let index = 0; index < 32; index++) {
505
+ bytes[index] = Number(scalar & 255n);
506
+ scalar >>= 8n;
507
+ }
508
+ return bytes;
509
+ }
510
+ return bytesFromList(term, env, 32);
511
+ }
512
+
513
+ function* curve25519MultBuiltin({ goal, env }) {
514
+ const crypto = requireCrypto();
515
+ const scalar = scalar25519Bytes(goal.args[0], env);
516
+ const point = byteChars(goal.args[1], env, 32);
517
+ let result;
518
+ try {
519
+ const privateKey = crypto.createPrivateKey({ key: Buffer.concat([X25519_PRIVATE_PREFIX, scalar]), format: 'der', type: 'pkcs8' });
520
+ const publicKey = crypto.createPublicKey({ key: Buffer.concat([X25519_PUBLIC_PREFIX, point]), format: 'der', type: 'spki' });
521
+ result = crypto.diffieHellman({ privateKey, publicKey });
522
+ } catch { return; }
523
+ const next = bindResult(goal.args[2], byteCharsTerm(result), env);
524
+ if (next) yield next;
525
+ }
526
+
527
+ function mod(value, modulus) {
528
+ const result = value % modulus;
529
+ return result >= 0n ? result : result + modulus;
530
+ }
531
+
532
+ function modPow(base, exponent, modulus) {
533
+ let result = 1n, factor = mod(base, modulus), power = exponent;
534
+ while (power > 0n) {
535
+ if (power & 1n) result = mod(result * factor, modulus);
536
+ factor = mod(factor * factor, modulus);
537
+ power >>= 1n;
538
+ }
539
+ return result;
540
+ }
541
+
542
+ function modInverse(value, modulus) {
543
+ const normalized = mod(value, modulus);
544
+ if (normalized === 0n) throw new Error('inverse of zero');
545
+ return modPow(normalized, modulus - 2n, modulus);
546
+ }
547
+
548
+ function secpAdd(left, right) {
549
+ if (left == null) return right;
550
+ if (right == null) return left;
551
+ const [x1, y1] = left, [x2, y2] = right;
552
+ if (x1 === x2 && mod(y1 + y2, SECP256K1_P) === 0n) return null;
553
+ let slope;
554
+ if (x1 === x2 && y1 === y2) {
555
+ slope = mod((3n * x1 * x1) * modInverse(2n * y1, SECP256K1_P), SECP256K1_P);
556
+ } else {
557
+ slope = mod((y2 - y1) * modInverse(x2 - x1, SECP256K1_P), SECP256K1_P);
558
+ }
559
+ const x3 = mod(slope * slope - x1 - x2, SECP256K1_P);
560
+ const y3 = mod(slope * (x1 - x3) - y1, SECP256K1_P);
561
+ return [x3, y3];
562
+ }
563
+
564
+ function secpMultiply(scalar, point) {
565
+ let n = mod(scalar, SECP256K1_N), addend = point, result = null;
566
+ while (n > 0n) {
567
+ if (n & 1n) result = secpAdd(result, addend);
568
+ addend = secpAdd(addend, addend);
569
+ n >>= 1n;
570
+ }
571
+ return result;
572
+ }
573
+
574
+ function pointTerm(term, env) {
575
+ const value = deref(term, env);
576
+ if (value.type !== COMPOUND || value.name !== 'point' || value.arity !== 2) {
577
+ throw new PrologError('type_error(point)', copyResolved(value, env));
578
+ }
579
+ return [integerTermValue(value.args[0], env), integerTermValue(value.args[1], env)];
580
+ }
581
+
582
+ function secpCurveName(term, env) {
583
+ const value = deref(term, env);
584
+ if (value.type !== COMPOUND || value.name !== 'curve' || value.arity !== 8) {
585
+ throw new PrologError('type_error(curve)', copyResolved(value, env));
586
+ }
587
+ const name = deref(value.args[0], env);
588
+ if (name.type !== ATOM || name.name !== 'secp256k1') {
589
+ throw new PrologError('domain_error(elliptic_curve)', copyResolved(value, env));
590
+ }
591
+ }
592
+
593
+ function* curveScalarMultBuiltin({ goal, env }) {
594
+ secpCurveName(goal.args[0], env);
595
+ const scalar = integerTermValue(goal.args[1], env);
596
+ if (scalar <= 0n) throw new PrologError('domain_error(not_less_than_one)', copyResolved(goal.args[1], env));
597
+ const point = pointTerm(goal.args[2], env);
598
+ const [x, y] = point;
599
+ if (x < 0n || x >= SECP256K1_P || y < 0n || y >= SECP256K1_P ||
600
+ mod(y * y - (x * x * x + 7n), SECP256K1_P) !== 0n) {
601
+ throw new PrologError('domain_error(point_on_curve)', copyResolved(goal.args[2], env));
602
+ }
603
+ const result = secpMultiply(scalar, point);
604
+ if (result == null) throw new PrologError('domain_error(point_at_infinity)');
605
+ const next = bindResult(goal.args[3], compound('point', [numberTerm(result[0]), numberTerm(result[1])]), env);
606
+ if (next) yield next;
607
+ }
608
+
609
+ export const cryptoHostBuiltins = {
610
+ register(registry) {
611
+ registry.add('eyeprolog__hex_bytes', 2, hexBytesBuiltin, { deterministic: true, eyePrologLibrary: true });
612
+ registry.add('eyeprolog__crypto_n_random_bytes', 2, randomBytesBuiltin, { deterministic: true, eyePrologLibrary: true });
613
+ registry.add('eyeprolog__crypto_data_hash', 3, dataHashBuiltin, { deterministic: true, eyePrologLibrary: true });
614
+ registry.add('eyeprolog__crypto_data_hkdf', 4, hkdfBuiltin, { deterministic: true, eyePrologLibrary: true });
615
+ registry.add('eyeprolog__crypto_password_hash', 2, passwordHash2Builtin, { deterministic: true, eyePrologLibrary: true });
616
+ registry.add('eyeprolog__crypto_password_hash', 3, passwordHash3Builtin, { deterministic: true, eyePrologLibrary: true });
617
+ registry.add('eyeprolog__crypto_data_encrypt', 6, encryptBuiltin, { deterministic: true, eyePrologLibrary: true });
618
+ registry.add('eyeprolog__crypto_data_decrypt', 6, decryptBuiltin, { deterministic: true, eyePrologLibrary: true });
619
+ registry.add('eyeprolog__ed25519_seed_keypair', 2, edSeedPairBuiltin, { deterministic: true, eyePrologLibrary: true });
620
+ registry.add('eyeprolog__ed25519_new_keypair', 1, edNewPairBuiltin, { deterministic: true, eyePrologLibrary: true });
621
+ registry.add('eyeprolog__ed25519_keypair_public_key', 2, edPublicBuiltin, { deterministic: true, eyePrologLibrary: true });
622
+ registry.add('eyeprolog__ed25519_sign', 4, edSignBuiltin, { deterministic: true, eyePrologLibrary: true });
623
+ registry.add('eyeprolog__ed25519_verify', 4, edVerifyBuiltin, { deterministic: true, eyePrologLibrary: true });
624
+ registry.add('eyeprolog__curve25519_generator', 1, curve25519GeneratorBuiltin, { deterministic: true, eyePrologLibrary: true });
625
+ registry.add('eyeprolog__curve25519_scalar_mult', 3, curve25519MultBuiltin, { deterministic: true, eyePrologLibrary: true });
626
+ registry.add('eyeprolog__crypto_curve_scalar_mult', 4, curveScalarMultBuiltin, { deterministic: true, eyePrologLibrary: true });
627
+ },
628
+ };
@@ -1,6 +1,12 @@
1
1
  /** Integer arithmetic helpers used by Scryer's CLP(Z) source. */
2
2
 
3
- :- module(arithmetic, [lsb/2, msb/2, popcount/2]).
3
+ :- module(arithmetic, [
4
+ lsb/2,
5
+ msb/2,
6
+ number_to_rational/2,
7
+ popcount/2,
8
+ rational_numerator_denominator/3
9
+ ]).
4
10
 
5
11
  :- use_module(library(error), [must_be/2, domain_error/3]).
6
12
 
@@ -34,3 +40,13 @@ arithmetic__popcount(X, Count0, Count) :-
34
40
  Count1 is Count0 + Bit,
35
41
  X1 is X >> 1,
36
42
  arithmetic__popcount(X1, Count1, Count).
43
+
44
+
45
+ % Rational-form compatibility. EyeProlog currently has integer and float
46
+ % processor values; non-integral results are represented canonically as the
47
+ % ordinary term rdiv(Numerator, Denominator).
48
+ number_to_rational(Number, Rational) :-
49
+ eyeprolog__number_to_rational(Number, Rational).
50
+
51
+ rational_numerator_denominator(Rational, Numerator, Denominator) :-
52
+ eyeprolog__rational_numerator_denominator(Rational, Numerator, Denominator).