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 +1 -1
- package/package.json +1 -1
- package/src/crypto-host.js +628 -0
- package/src/lib/crypto.pl +91 -0
- package/src/library-autoload-index.js +20 -0
- package/src/standard-library.js +10 -0
- package/test/run-regression.mjs +60 -7
- package/the-art-of-eyeprolog.md +23 -8
- package/why-eyeprolog.md +11 -6
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,
|
|
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
|
@@ -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",
|
package/src/standard-library.js
CHANGED
|
@@ -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
|
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -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,
|
|
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,
|
|
6381
|
-
assertEqual(eyePrologPortableLibraryIndicators.length,
|
|
6382
|
-
assertEqual(eyePrologInteropLibraryIndicators.length,
|
|
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,
|
|
6396
|
-
assertEqual(eyePrologNativeLibraryIndicators.length,
|
|
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,
|
|
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');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6293,12 +6293,12 @@ so side effects occur in Prolog execution order.
|
|
|
6293
6293
|
|
|
6294
6294
|
### The EyeProlog library
|
|
6295
6295
|
|
|
6296
|
-
EyeProlog exposes **
|
|
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
|
-
**
|
|
6299
|
-
`src/lib/`; **
|
|
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 **
|
|
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.
|
|
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.
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|