eyeprolog 1.5.8 → 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,7 +61,7 @@ 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, and focused `library(files)` and `library(os)` modules. Filesystem and OS side effects require Node; browser calls report an unavailable host resource instead. Non-integral `number_to_rational/2` results are represented structurally as `rdiv(Numerator,Denominator)` until rational numbers become processor numeric values.
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
65
 
66
66
  ## Links
67
67
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.8",
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",
@@ -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
+ };
@@ -0,0 +1,91 @@
1
+ /** Cryptographic predicates compatible with Scryer's library(crypto).
2
+
3
+ The public predicate and data-format surface follows Scryer Prolog. Sensitive
4
+ primitives are delegated to EyeProlog's private host adapter, which uses Node's
5
+ cryptographic backend. Hex conversion and curve metadata retain the same Prolog
6
+ representations as Scryer.
7
+ */
8
+
9
+ :- module(crypto, [
10
+ hex_bytes/2,
11
+ crypto_n_random_bytes/2,
12
+ crypto_data_hash/3,
13
+ crypto_data_hkdf/4,
14
+ crypto_password_hash/2,
15
+ crypto_password_hash/3,
16
+ crypto_data_encrypt/6,
17
+ crypto_data_decrypt/6,
18
+ ed25519_seed_keypair/2,
19
+ ed25519_new_keypair/1,
20
+ ed25519_keypair_public_key/2,
21
+ ed25519_sign/4,
22
+ ed25519_verify/4,
23
+ curve25519_generator/1,
24
+ curve25519_scalar_mult/3,
25
+ crypto_name_curve/2,
26
+ crypto_curve_order/2,
27
+ crypto_curve_generator/2,
28
+ crypto_curve_scalar_mult/4
29
+ ]).
30
+
31
+ hex_bytes(Hex, Bytes) :-
32
+ eyeprolog__hex_bytes(Hex, Bytes).
33
+
34
+ crypto_n_random_bytes(N, Bytes) :-
35
+ eyeprolog__crypto_n_random_bytes(N, Bytes).
36
+
37
+ crypto_data_hash(Data, Hash, Options) :-
38
+ eyeprolog__crypto_data_hash(Data, Hash, Options).
39
+
40
+ crypto_data_hkdf(Data, Length, Bytes, Options) :-
41
+ eyeprolog__crypto_data_hkdf(Data, Length, Bytes, Options).
42
+
43
+ crypto_password_hash(Password, Hash) :-
44
+ eyeprolog__crypto_password_hash(Password, Hash).
45
+
46
+ crypto_password_hash(Password, Hash, Options) :-
47
+ eyeprolog__crypto_password_hash(Password, Hash, Options).
48
+
49
+ crypto_data_encrypt(PlainText, Algorithm, Key, IV, CipherText, Options) :-
50
+ eyeprolog__crypto_data_encrypt(PlainText, Algorithm, Key, IV, CipherText, Options).
51
+
52
+ crypto_data_decrypt(CipherText, Algorithm, Key, IV, PlainText, Options) :-
53
+ eyeprolog__crypto_data_decrypt(CipherText, Algorithm, Key, IV, PlainText, Options).
54
+
55
+ ed25519_seed_keypair(Seed, KeyPair) :-
56
+ eyeprolog__ed25519_seed_keypair(Seed, KeyPair).
57
+
58
+ ed25519_new_keypair(KeyPair) :-
59
+ eyeprolog__ed25519_new_keypair(KeyPair).
60
+
61
+ ed25519_keypair_public_key(KeyPair, PublicKey) :-
62
+ eyeprolog__ed25519_keypair_public_key(KeyPair, PublicKey).
63
+
64
+ ed25519_sign(KeyPair, Data, Signature, Options) :-
65
+ eyeprolog__ed25519_sign(KeyPair, Data, Signature, Options).
66
+
67
+ ed25519_verify(PublicKey, Data, Signature, Options) :-
68
+ eyeprolog__ed25519_verify(PublicKey, Data, Signature, Options).
69
+
70
+ curve25519_generator(Generator) :-
71
+ eyeprolog__curve25519_generator(Generator).
72
+
73
+ curve25519_scalar_mult(Scalar, Point, Result) :-
74
+ eyeprolog__curve25519_scalar_mult(Scalar, Point, Result).
75
+
76
+ crypto_curve_generator(curve(_,_,_,_,Generator,_,_,_), Generator).
77
+ crypto_curve_order(curve(_,_,_,_,_,Order,_,_), Order).
78
+
79
+ crypto_curve_scalar_mult(Curve, Scalar, Point, Result) :-
80
+ eyeprolog__crypto_curve_scalar_mult(Curve, Scalar, Point, Result).
81
+
82
+ crypto_name_curve(secp256k1,
83
+ curve(secp256k1,
84
+ 0x00fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f,
85
+ 0x0,
86
+ 0x7,
87
+ point(0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,
88
+ 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8),
89
+ 0x00fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141,
90
+ 32,
91
+ 1)).
@@ -92,9 +92,22 @@ export const eyePrologLibraryAutoload = Object.freeze({
92
92
  "contains/2": "strings",
93
93
  "copy_term_nat/2": "terms",
94
94
  "countall/2": "iso_ext",
95
+ "crypto_curve_generator/2": "crypto",
96
+ "crypto_curve_order/2": "crypto",
97
+ "crypto_curve_scalar_mult/4": "crypto",
98
+ "crypto_data_decrypt/6": "crypto",
99
+ "crypto_data_encrypt/6": "crypto",
100
+ "crypto_data_hash/3": "crypto",
101
+ "crypto_data_hkdf/4": "crypto",
102
+ "crypto_n_random_bytes/2": "crypto",
103
+ "crypto_name_curve/2": "crypto",
104
+ "crypto_password_hash/2": "crypto",
105
+ "crypto_password_hash/3": "crypto",
95
106
  "cumulative/1": "clpz",
96
107
  "cumulative/2": "clpz",
97
108
  "current_time/1": "time",
109
+ "curve25519_generator/1": "crypto",
110
+ "curve25519_scalar_mult/3": "crypto",
98
111
  "debug/1": "debug",
99
112
  "debug/3": "debug",
100
113
  "del_assoc/4": "assoc",
@@ -113,6 +126,11 @@ export const eyePrologLibraryAutoload = Object.freeze({
113
126
  "domain_error/2": "error",
114
127
  "domain_error/3": "error",
115
128
  "drop/3": "lists",
129
+ "ed25519_keypair_public_key/2": "crypto",
130
+ "ed25519_new_keypair/1": "crypto",
131
+ "ed25519_seed_keypair/2": "crypto",
132
+ "ed25519_sign/4": "crypto",
133
+ "ed25519_verify/4": "crypto",
116
134
  "edges/2": "ugraphs",
117
135
  "element/3": "clpz",
118
136
  "empty_assoc/1": "assoc",
@@ -148,6 +166,7 @@ export const eyePrologLibraryAutoload = Object.freeze({
148
166
  "global_cardinality/3": "clpz",
149
167
  "group_pairs_by_key/2": "pairs",
150
168
  "gt/2": "comparison",
169
+ "hex_bytes/2": "crypto",
151
170
  "if_/3": "reif",
152
171
  "in/2": "clpz",
153
172
  "indomain/1": "clpz",
@@ -343,6 +362,7 @@ export const eyePrologLibraryAutoloadModules = Object.freeze([
343
362
  "clpb",
344
363
  "clpz",
345
364
  "comparison",
365
+ "crypto",
346
366
  "dates",
347
367
  "dcgs",
348
368
  "debug",
@@ -7,6 +7,7 @@ import { attsBuiltins } from './atts.js';
7
7
  import { expansionBuiltins } from './expansion-builtins.js';
8
8
  import { scryerCompatibilityBuiltins } from './scryer-compat.js';
9
9
  import { libraryHostBuiltins } from './library-host.js';
10
+ import { cryptoHostBuiltins } from './crypto-host.js';
10
11
  import { registerCleanupBuiltins } from './cleanup.js';
11
12
  import { fs, isNode, memoryStatistics } from './platform.js';
12
13
  import { ATOM, VAR, atom, deref, numberTerm, unify } from './term.js';
@@ -24,6 +25,7 @@ const moduleFiles = Object.freeze({
24
25
  clpb: 'clpb.pl',
25
26
  clpz: 'clpz.pl',
26
27
  comparison: 'comparison.pl',
28
+ crypto: 'crypto.pl',
27
29
  dates: 'dates.pl',
28
30
  dcgs: 'dcgs.pl',
29
31
  debug: 'debug.pl',
@@ -86,6 +88,11 @@ export const eyePrologNativeLibraryIndicators = Object.freeze([
86
88
  'sleep/1',
87
89
  'directory_files/2', 'delete_file/1', 'rename_file/2', 'make_directory/1', 'make_directory_path/1', 'working_directory/2',
88
90
  'getenv/2', 'setenv/2', 'unsetenv/1', 'shell/1', 'shell/2', 'pid/1', 'raw_argv/1', 'argv/1',
91
+ 'hex_bytes/2', 'crypto_n_random_bytes/2', 'crypto_data_hash/3', 'crypto_data_hkdf/4',
92
+ 'crypto_password_hash/2', 'crypto_password_hash/3', 'crypto_data_encrypt/6', 'crypto_data_decrypt/6',
93
+ 'ed25519_seed_keypair/2', 'ed25519_new_keypair/1', 'ed25519_keypair_public_key/2',
94
+ 'ed25519_sign/4', 'ed25519_verify/4', 'curve25519_generator/1', 'curve25519_scalar_mult/3',
95
+ 'crypto_curve_scalar_mult/4',
89
96
  'put_atts/2', 'get_atts/2', 'put_attr/3', 'get_attr/3', 'del_attr/2', 'term_attributed_variables/2', 'call_residue_vars/2',
90
97
  '#>/2', '#</2', '#>=/2', '#=</2', '#=/2', '#\\=/2', '#\\/1',
91
98
  '#<==>/2', '#==>/2', '#<==/2', '#\\//2', '#\\/2', '#/\\/2',
@@ -111,6 +118,7 @@ export const eyePrologPortableLibraryIndicators = Object.freeze([
111
118
  'sat/1', 'taut/2', 'labeling/1', 'sat_count/2', 'random_labeling/2', 'weighted_maximum/3',
112
119
  'lt/2', 'gt/2', 'le/2', 'ge/2',
113
120
  'difference/3',
121
+ 'crypto_name_curve/2', 'crypto_curve_order/2', 'crypto_curve_generator/2',
114
122
  'stable/1', 'becomes/2',
115
123
  'seq/3', 'seqq/3',
116
124
  'debug/1', 'debug/3', 'nodebug/1', 'bb_get/2', 'bb_put/2', 'bb_b_put/2', 'bb_global_get/2',
@@ -256,6 +264,7 @@ const eyePrologSharedLibraryIndicators = [
256
264
  'tfilter/3', 'tmember/2', 'tmember_t/3', 'tpartition/4',
257
265
  'start_tabling/2', 'abolish_all_tables/0', 'sleep/1', 'current_time/1', 'format_time/4', 'statistics/2',
258
266
  'getenv/2', 'setenv/2', 'unsetenv/1', 'shell/1', 'shell/2', 'pid/1', 'raw_argv/1', 'argv/1',
267
+ 'hex_bytes/2', 'crypto_n_random_bytes/2', 'crypto_data_hash/3',
259
268
  'add_edges/3', 'add_vertices/3', 'complement/2', 'compose/3', 'connect_ugraph/3',
260
269
  'del_edges/3', 'del_vertices/3', 'edges/2', 'neighbors/3', 'neighbours/3',
261
270
  'reachable/3', 'top_sort/2', 'top_sort/3', 'transitive_closure/2',
@@ -337,6 +346,7 @@ export function createEyePrologRegistry() {
337
346
  expansionBuiltins.register(registry);
338
347
  scryerCompatibilityBuiltins.register(registry);
339
348
  libraryHostBuiltins.register(registry);
349
+ cryptoHostBuiltins.register(registry);
340
350
  registry.eyePrologLibrary = true;
341
351
  return registry;
342
352
  }
@@ -5747,6 +5747,59 @@ answer(Status,Pid) :-
5747
5747
  assertEqual(fs.existsSync(nested), true, 'make_directory_path/1 created nested directories');
5748
5748
  },
5749
5749
  },
5750
+ {
5751
+ name: 'library(crypto) matches deterministic hash, HMAC, and HKDF vectors',
5752
+ run: () => {
5753
+ const source = `:- use_module(library(crypto)).
5754
+ answer(Bytes) :-
5755
+ hex_bytes("501ACE",[80,26,206]),
5756
+ crypto_data_hash("abc","ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",[algorithm(sha256)]),
5757
+ crypto_data_hash("abc","9c196e32dc0175f86f4b1cb89289d6619de6bee699e4c378e68309ed97a1a6ab",[algorithm(sha256),hmac([107,101,121])]),
5758
+ crypto_data_hkdf("ikm",16,Bytes,[algorithm(sha256),salt([1,2,3]),info("ctx")]).
5759
+ `;
5760
+ const result = run(source, { goal: 'answer(Bytes)' });
5761
+ assertEqual(result.stdout,
5762
+ 'answer([88, 155, 48, 74, 204, 30, 221, 100, 71, 23, 177, 120, 135, 159, 145, 52]).\n',
5763
+ 'crypto vectors');
5764
+ },
5765
+ },
5766
+ {
5767
+ name: 'library(crypto) supports password verification and authenticated ChaCha20-Poly1305 round trips',
5768
+ run: () => {
5769
+ const source = `:- use_module(library(crypto)).
5770
+ check :-
5771
+ crypto_password_hash("secret",Hash,[cost(4),salt([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15])]),
5772
+ crypto_password_hash("secret",Hash),
5773
+ Key=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31],
5774
+ IV=[0,1,2,3,4,5,6,7,8,9,10,11],
5775
+ crypto_data_encrypt("hello",'chacha20-poly1305',Key,IV,Cipher,[tag(Tag),aad("meta")]),
5776
+ crypto_data_decrypt(Cipher,'chacha20-poly1305',Key,IV,"hello",[tag(Tag),aad("meta")]).
5777
+ `;
5778
+ assertEqual(run(source, { goal: 'check' }).stdout, 'check.\n', 'crypto round trip');
5779
+ },
5780
+ },
5781
+ {
5782
+ name: 'library(crypto) supports Ed25519, X25519, and secp256k1 compatibility operations',
5783
+ run: () => {
5784
+ const source = `:- use_module(library(crypto)).
5785
+ :- use_module(library(lists), [maplist/3]).
5786
+ check :-
5787
+ hex_bytes("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",Seed),
5788
+ ed25519_seed_keypair(Seed,Pair), ed25519_keypair_public_key(Pair,Public),
5789
+ maplist(char_code,Public,PublicBytes),
5790
+ hex_bytes("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",PublicBytes),
5791
+ ed25519_sign(Pair,[],"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b",[]),
5792
+ curve25519_generator(G),
5793
+ A=[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
5794
+ B=[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
5795
+ curve25519_scalar_mult(A,G,AP), curve25519_scalar_mult(B,G,BP),
5796
+ curve25519_scalar_mult(A,BP,Shared), curve25519_scalar_mult(B,AP,Shared),
5797
+ crypto_name_curve(secp256k1,Curve), crypto_curve_generator(Curve,Generator),
5798
+ crypto_curve_scalar_mult(Curve,2,Generator,point(89565891926547004231252920425935692360644145829622209833684329913297188986597,12158399299693830322967808612713398636155367887041628176798871954788371653930)).
5799
+ `;
5800
+ assertEqual(run(source, { goal: 'check' }).stdout, 'check.\n', 'elliptic-curve compatibility');
5801
+ },
5802
+ },
5750
5803
  {
5751
5804
  name: 'library(time) and library(iso_ext) expose their shared compatibility re-exports',
5752
5805
  run: () => {
@@ -6354,7 +6407,7 @@ answer(ok) :-
6354
6407
  assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
6355
6408
  assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
6356
6409
  assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
6357
- assertEqual(library.defs.size, 185, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS tnot/1, and generic library adapters');
6410
+ assertEqual(library.defs.size, 201, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS tnot/1, and generic library adapters');
6358
6411
  assertEqual(registry.get('eyeprolog__dynify', 1), null, 'Eyelet dynify adapter is absent from the ISO registry');
6359
6412
  assertEqual(Boolean(library.get('eyeprolog__dynify', 1)), true, 'Eyelet dynify adapter is an internal EyeProlog library primitive');
6360
6413
  assertEqual(registry.get('eyeprolog__eyelet_emit', 2), null, 'Eyelet event adapter is absent from the ISO registry');
@@ -6377,9 +6430,9 @@ answer(ok) :-
6377
6430
  assertEqual(Boolean(library.get('call_cleanup', 2)), true, 'call_cleanup/2 is an EyeProlog cleanup control');
6378
6431
  assertEqual(registry.get('setup_call_cleanup', 3), null, 'setup_call_cleanup/3 is absent from the ISO registry');
6379
6432
  assertEqual(Boolean(library.get('setup_call_cleanup', 3)), true, 'setup_call_cleanup/3 is an EyeProlog cleanup control');
6380
- assertEqual(registeredNativeEyePrologLibraryNames().length, 82, 'public host-supported EyeProlog library count');
6381
- assertEqual(eyePrologPortableLibraryIndicators.length, 243, 'portable Prolog library count');
6382
- assertEqual(eyePrologInteropLibraryIndicators.length, 214, 'cross-implementation interop profile count');
6433
+ assertEqual(registeredNativeEyePrologLibraryNames().length, 98, 'public host-supported EyeProlog library count');
6434
+ assertEqual(eyePrologPortableLibraryIndicators.length, 246, 'portable Prolog library count');
6435
+ assertEqual(eyePrologInteropLibraryIndicators.length, 217, 'cross-implementation interop profile count');
6383
6436
  assertEqual(eyePrologInteropLibraryModules.length, 25, 'common explicit library module profile count');
6384
6437
  assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
6385
6438
  assertEqual(eyePrologInteropAutoload['between/3'], 'between', 'between/3 canonical internal autoload');
@@ -6392,11 +6445,11 @@ answer(ok) :-
6392
6445
  assertEqual(eyePrologLibraryAutoload['pairs_keys_values/3'], 'pairs', 'complete library autoload includes library(pairs)');
6393
6446
  assertEqual(eyePrologLibraryAutoload['stable/1'], 'eyelet', 'complete library autoload includes Eyelet stable/1');
6394
6447
  assertEqual(eyePrologLibraryAutoload['becomes/2'], 'eyelet', 'complete library autoload includes Eyelet becomes/2');
6395
- assertEqual(eyePrologLibraryAutoloadModules.length, 38, 'all bundled src/lib modules are indexed for autoload');
6396
- assertEqual(eyePrologNativeLibraryIndicators.length, 82, 'host-supported library count');
6448
+ assertEqual(eyePrologLibraryAutoloadModules.length, 39, 'all bundled src/lib modules are indexed for autoload');
6449
+ assertEqual(eyePrologNativeLibraryIndicators.length, 98, 'host-supported library count');
6397
6450
  assertEqual(eyePrologNativeLibraryIndicators.includes('call_nth/2'), true, 'call_nth/2 remains classified as host-supported');
6398
6451
  assertEqual(eyePrologNativeLibraryIndicators.includes('random/1'), true, 'stateful random/1 is classified as host-supported');
6399
- assertEqual(eyePrologLibraryIndicators.length, 325, 'complete non-ISO EyeProlog library surface');
6452
+ assertEqual(eyePrologLibraryIndicators.length, 344, 'complete non-ISO EyeProlog library surface');
6400
6453
  assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
6401
6454
  assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
6402
6455
  assertEqual(Boolean(library.get('eyeprolog__call_residue_vars', 2)), true, 'private call_residue_vars adapter is registered for EyeProlog');
@@ -6293,12 +6293,12 @@ so side effects occur in Prolog execution order.
6293
6293
 
6294
6294
  ### The EyeProlog library
6295
6295
 
6296
- EyeProlog exposes **325 distinct non-ISO library and normal-extension predicate
6296
+ EyeProlog exposes **344 distinct non-ISO library and normal-extension predicate
6297
6297
  indicators** in addition to the 129 indicators in its isolated ISO profile.
6298
- **243 are defined entirely as ordinary Prolog clauses** in focused modules under
6299
- `src/lib/`; **82 use host support** for control, attributed variables,
6298
+ **246 are defined entirely as ordinary Prolog clauses** in focused modules under
6299
+ `src/lib/`; **98 use host support** for control, attributed variables,
6300
6300
  constraints, character conversion, filesystem/OS access, timing, or
6301
- observability. The ISO and library catalogs therefore cover **454 distinct predicate indicators**. A normal-runtime predicate that is intentionally
6301
+ observability. The ISO and library catalogs therefore cover **473 distinct predicate indicators**. A normal-runtime predicate that is intentionally
6302
6302
  re-exported by a compatibility module is counted once in this library surface:
6303
6303
  for example `call_cleanup/2` and `setup_call_cleanup/3` are exported by
6304
6304
  `library(iso_ext)`, while `time/1` and `statistics/2` are available from
@@ -6309,7 +6309,7 @@ registry.
6309
6309
  The sources are `src/lib/aggregate.pl`, `src/lib/arithmetic.pl`,
6310
6310
  `src/lib/assoc.pl`, `src/lib/atts.pl`, `src/lib/between.pl`,
6311
6311
  `src/lib/charsio.pl`, `src/lib/clpb.pl`, `src/lib/clpz.pl`,
6312
- `src/lib/comparison.pl`, `src/lib/dates.pl`, `src/lib/dcgs.pl`,
6312
+ `src/lib/comparison.pl`, `src/lib/crypto.pl`, `src/lib/dates.pl`, `src/lib/dcgs.pl`,
6313
6313
  `src/lib/debug.pl`, `src/lib/dif.pl`, `src/lib/error.pl`,
6314
6314
  `src/lib/eyelet.pl`, `src/lib/files.pl`, `src/lib/format.pl`,
6315
6315
  `src/lib/freeze.pl`, `src/lib/gensym.pl`, `src/lib/iso_ext.pl`,
@@ -6329,7 +6329,9 @@ and `freeze` modules. The published Prologue, `call_nth/2`, and `length/2`
6329
6329
  quads are retained as offline regressions. `src/standard-library.js` registers the module sources and private control and
6330
6330
  constraint adapters for Node and browser resolution. `src/library-host.js`
6331
6331
  contains the small character-I/O, Base64, rational-form, sleep, filesystem, and
6332
- OS bridges used by the corresponding Prolog modules. Explicit `use_module/1-2`
6332
+ OS bridges used by the corresponding Prolog modules. `src/crypto-host.js` keeps
6333
+ the cryptographic host primitives separate from the public `library(crypto)`
6334
+ module. Explicit `use_module/1-2`
6333
6335
  loads remain supported; outside strict ISO mode, the bundled-library autoloader
6334
6336
  may also load the canonical owner of any exported `src/lib/` predicate. The
6335
6337
  autoload index is generated from the libraries' `module/2` declarations.
@@ -6381,6 +6383,7 @@ bindings make one aligned subterm pair sufficient. For example:
6381
6383
  | `library(clpb)` | `labeling/1`, `random_labeling/2`, `sat/1`, `sat_count/2`, `taut/2`, `weighted_maximum/3` | Boolean constraints and BDD reasoning; upstream Prolog source with small state adapters |
6382
6384
  | `library(clpz)` | `#>/2`, `#</2`, `#>=/2`, `#=</2`, `#=/2`, `#\=/2`, `#\/1`, `#<==>/2`, `#==>/2`, `#<==/2`, `#\//2`, `#\/2`, `#/\/2`, `in/2`, `ins/2`, `all_different/1`, `all_distinct/1`, `nvalue/2`, `sum/3`, `scalar_product/4`, `tuples_in/2`, `labeling/2`, `label/1`, `indomain/1`, `lex_chain/1`, `serialized/2`, `global_cardinality/2`, `global_cardinality/3`, `circuit/1`, `cumulative/1`, `cumulative/2`, `disjoint2/1`, `element/3`, `automaton/3`, `automaton/8`, `zcompare/3`, `chain/2`, `fd_var/1`, `fd_inf/2`, `fd_sup/2`, `fd_size/2`, `fd_dom/2`, `clpz_t/2`, `#=/3`, `#</3` | Constraint logic programming over integers; the final three indicators support reified compatibility libraries |
6383
6385
  | `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` | Generic comparison |
6386
+ | `library(crypto)` | `crypto_curve_generator/2`, `crypto_curve_order/2`, `crypto_curve_scalar_mult/4`, `crypto_data_decrypt/6`, `crypto_data_encrypt/6`, `crypto_data_hash/3`, `crypto_data_hkdf/4`, `crypto_n_random_bytes/2`, `crypto_name_curve/2`, `crypto_password_hash/2`, `crypto_password_hash/3`, `curve25519_generator/1`, `curve25519_scalar_mult/3`, `ed25519_keypair_public_key/2`, `ed25519_new_keypair/1`, `ed25519_seed_keypair/2`, `ed25519_sign/4`, `ed25519_verify/4`, `hex_bytes/2` | Scryer-compatible hashing, KDFs, authenticated encryption, Ed25519, X25519, and secp256k1 helpers |
6384
6387
  | `library(dates)` | `difference/3` | ISO duration differences |
6385
6388
  | `library(dcgs)` | `phrase/2`, `phrase/3`, `seq/3`, `seqq/3` | DCG compatibility; `seq/3` and `seqq/3` are declared as `seq //1` and `seqq //1` |
6386
6389
  | `library(debug)` | `*/1`, `$/1`, `$-/1`, `debug/1`, `debug/3`, `nodebug/1`, `bb_get/2`, `bb_put/2`, `bb_b_put/2`, `bb_global_get/2` | Declarative debug operators and constraint-library blackboards |
@@ -6436,7 +6439,7 @@ predicate indicators also documented by Trealla. Where Trealla exposes a
6436
6439
  predicate globally rather than from the same module, EyeProlog follows Scryer's
6437
6440
  module name so explicit Scryer-style imports remain available. The current
6438
6441
  module set includes `arithmetic`, `assoc`, `atts`, `charsio`, `clpb`, `clpz`,
6439
- `debug`, `dif`, `files`, `format`, `freeze`, `gensym`, `iso_ext`, `lambda`,
6442
+ `crypto`, `debug`, `dif`, `files`, `format`, `freeze`, `gensym`, `iso_ext`, `lambda`,
6440
6443
  `lists`, `ordsets`, `os`, `pio`, `random`, `reif`, `tabling`, `time`, `ugraphs`,
6441
6444
  `uuid`, and `when`. The predicate profile is narrower than
6442
6445
  the union of their exports: for example, EyeProlog's explicit-state `random/3`
@@ -6450,7 +6453,9 @@ closure adaptations. `dif.pl`, `tabling.pl`, and part of `time.pl` are thin
6450
6453
  facades over EyeProlog runtime facilities. `charsio.pl` adds the common
6451
6454
  chars/term and Base64 relations, while `pio.pl` provides eager DCG I/O.
6452
6455
  `files.pl` and `os.pl` expose the selected Trealla/Scryer host-service
6453
- intersection through Node adapters. The
6456
+ intersection through Node adapters. `crypto.pl` exposes Scryer's complete
6457
+ public crypto surface; its strict Trealla/Scryer overlap is `hex_bytes/2`,
6458
+ `crypto_n_random_bytes/2`, and `crypto_data_hash/3`. The
6454
6459
  [portable library overlap example](https://github.com/eyereasoner/eyeprolog/blob/main/examples/portable-library-overlap.pl)
6455
6460
  composes Boolean constraints, ordered sets, graphs, reification, delayed goals,
6456
6461
  generated names, character conversion, transposition, and explicit table
@@ -6478,6 +6483,16 @@ its host predicates use atoms instead. Both EyeProlog modules require the Node
6478
6483
  host; browser calls raise a resource error instead of simulating filesystem,
6479
6484
  environment, or process side effects.
6480
6485
 
6486
+ `library(crypto)` follows Scryer's character-list and byte-list conventions.
6487
+ It provides hexadecimal conversion, cryptographically secure random bytes,
6488
+ hashes and HMAC, HKDF, PBKDF2-SHA512 password hashes, ChaCha20-Poly1305,
6489
+ Ed25519 signing and verification, X25519 key agreement, and the Scryer
6490
+ secp256k1 curve representation/helpers. Hashing, KDF, authenticated encryption, Ed25519, and X25519 use Node's
6491
+ cryptographic backend; `crypto_n_random_bytes/2` can also use Web Crypto's
6492
+ CSPRNG. Operations without a suitable backend raise `resource_error(crypto)`.
6493
+ `hex_bytes/2` and the static curve metadata remain usable without that backend. As in Scryer, new key-agreement code should prefer X25519 over
6494
+ the older generic secp256k1 helper.
6495
+
6481
6496
  For `library(lists)`, the current interop predicate set is `member/2`,
6482
6497
  `memberchk/2`, `select/3`, `append/2-3`, `last/2`, `same_length/2`,
6483
6498
  `nth0/3-4`, `nth1/3-4`, `reverse/2`, `length/2`, `maplist/2-8`,
package/why-eyeprolog.md CHANGED
@@ -166,12 +166,17 @@ interfaces rather than by library names alone. EyeProlog includes the common
166
166
  Trealla/Scryer character-term conversion and Base64 predicates, the shared
167
167
  arithmetic conversion helpers, `sleep/1`, and selected `iso_ext` re-exports. It
168
168
  also provides Scryer-style `library(files)` and `library(os)` modules for
169
- predicate indicators that Trealla documents too. Their filesystem, environment,
170
- shell, PID, and argument operations are Node host services and therefore fail
171
- with a resource error in browser execution rather than inventing a virtual OS.
172
- EyeProlog still has integer and float processor numbers only, so non-integral
173
- rational conversion results use a documented structural `rdiv(N,D)` form rather
174
- than silently extending arithmetic semantics.
169
+ predicate indicators that Trealla documents too, plus the full Scryer
170
+ `library(crypto)` predicate surface. The strict Trealla/Scryer crypto overlap is
171
+ `hex_bytes/2`, `crypto_n_random_bytes/2`, and `crypto_data_hash/3`; EyeProlog
172
+ also supports Scryer's HKDF, password hashing, authenticated encryption,
173
+ Ed25519, X25519, and secp256k1 helpers. Hashing, KDF, authenticated encryption, Ed25519, and X25519 use
174
+ Node's crypto backend; secure random bytes also use Web Crypto when available,
175
+ and operations without a suitable backend report `resource_error(crypto)`. Filesystem, environment, shell, PID, and argument operations are
176
+ likewise Node host services rather than a simulated browser OS. EyeProlog still
177
+ has integer and float processor numbers only, so non-integral rational
178
+ conversion results use a documented structural `rdiv(N,D)` form rather than
179
+ silently extending arithmetic semantics.
175
180
 
176
181
  Definite clause grammars follow the ISO Part 3 difference-list model. Internal
177
182
  fast paths make deep finite sequence processing economical while preserving