-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathmodule.ae
More file actions
456 lines (427 loc) · 17.7 KB
/
Copy pathmodule.ae
File metadata and controls
456 lines (427 loc) · 17.7 KB
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
// std.cryptography - Cryptographic hash primitives + Base64 codec
// Import with: import std.cryptography
//
// Pure functions: bytes in, hex digest or Base64 string out. Binary-
// safe via explicit byte length (embedded NULs are fine; pass 0 to
// hash or encode an empty buffer). Streaming (incremental) digests are
// supported via the digest-context API (digest_new / digest_update /
// digest_final_*). HMAC, key derivation, symmetric ciphers, signing,
// URL-safe Base64, and PKCS#7 / PEM parsing remain deliberately out of
// scope — see docs/stdlib-vs-contrib.md for the "one obvious shape"
// criterion.
//
// When the Aether toolchain was built without OpenSSL, every wrapper
// returns a non-empty error string ("openssl unavailable") rather
// than crashing. Callers that need cryptographic functions should
// check the error slot.
import std.string
import std.bytes
import std.encoding
import std.cryptography.hmac
exports(
cryptography_sha1_hex_raw, cryptography_sha256_hex_raw,
cryptography_hash_hex_raw, cryptography_hash_supported,
cryptography_md4_hex_raw, cryptography_md5_hex_raw,
cryptography_hmac_sha256_hex_raw, cryptography_hmac_sha256_bytes_raw,
cryptography_md4_bytes_raw, cryptography_md5_bytes_raw,
cryptography_sha1_bytes_raw, cryptography_sha256_bytes_raw,
cryptography_hash_bytes_raw,
cryptography_get_binary_digest, cryptography_get_binary_digest_length,
cryptography_release_binary_digest,
cryptography_random_bytes_raw,
cryptography_get_random_bytes, cryptography_get_random_bytes_length,
cryptography_release_random_bytes,
cryptography_digest_new_raw, cryptography_digest_update_raw,
cryptography_digest_final_hex_raw, cryptography_digest_final_bytes_raw,
cryptography_digest_free_raw,
sha1_hex, sha256_hex, hash_hex, hash_supported,
md4_hex, md5_hex, hmac_sha256_hex, hmac_sha256_bytes,
md4_bytes, md5_bytes, sha1_bytes, sha256_bytes, hash_bytes,
random_bytes, random_hex, random_base64,
digest_new, digest_update, digest_final_hex, digest_final_bytes,
digest_free
)
extern cryptography_sha1_hex_raw(data: string, length: int) -> string
extern cryptography_sha256_hex_raw(data: string, length: int) -> string
extern cryptography_hash_hex_raw(algo: string, data: string, length: int) -> string
extern cryptography_hash_supported(algo: string) -> int
extern cryptography_md4_hex_raw(data: string, length: int) -> string
extern cryptography_md5_hex_raw(data: string, length: int) -> string
extern cryptography_hmac_sha256_hex_raw(key: string, key_len: int,
msg: string, msg_len: int) -> string
extern cryptography_hmac_sha256_bytes_raw(key: string, key_len: int,
msg: string, msg_len: int) -> int
extern cryptography_md4_bytes_raw(data: string, length: int) -> int
extern cryptography_md5_bytes_raw(data: string, length: int) -> int
extern cryptography_sha1_bytes_raw(data: string, length: int) -> int
extern cryptography_sha256_bytes_raw(data: string, length: int) -> int
extern cryptography_hash_bytes_raw(algo: string, data: string, length: int) -> int
extern cryptography_get_binary_digest() -> string
extern cryptography_get_binary_digest_length() -> int
extern cryptography_release_binary_digest()
extern cryptography_random_bytes_raw(n: int) -> int
extern cryptography_get_random_bytes() -> string
extern cryptography_get_random_bytes_length() -> int
extern cryptography_release_random_bytes()
extern cryptography_digest_new_raw(algo: string) -> ptr
extern cryptography_digest_update_raw(handle: ptr, data: string, length: int) -> int
extern cryptography_digest_final_hex_raw(handle: ptr) -> string
extern cryptography_digest_final_bytes_raw(handle: ptr) -> int
extern cryptography_digest_free_raw(handle: ptr)
extern string_new_with_length(data: string, length: int) -> ptr
// Compute the SHA-1 hex digest of the first `length` bytes of `data`.
// Returns (digest, "") on success, ("", error) when OpenSSL is
// unavailable or the hash context could not be created.
// SHA-1 is included for interop with legacy formats (Git object IDs,
// Subversion rep-stores, HMAC-SHA1 fixtures). Prefer SHA-256 for new
// work.
sha1_hex(data: string, length: int) -> {
out = cryptography_sha1_hex_raw(data, length)
if out == 0 {
return "", "openssl unavailable"
}
return out, ""
}
// Compute the SHA-256 hex digest of the first `length` bytes of `data`.
// Returns (digest, "") on success, ("", error) on failure.
sha256_hex(data: string, length: int) -> {
out = cryptography_sha256_hex_raw(data, length)
if out == 0 {
return "", "openssl unavailable"
}
return out, ""
}
// Algorithm-by-name digest dispatch. `algo` is a string like "sha1"
// or "sha256" — useful when the algorithm is config-driven rather
// than compile-time. Returns the same lowercase-hex shape as
// sha1_hex / sha256_hex, or ("", error) when:
// - OpenSSL isn't linked
// - the algorithm name is unknown ("unknown algorithm")
// - the digest computation itself fails
//
// Whatever names libcrypto recognizes work here ("sha384",
// "sha512", "sha3-256", ...), but the supported set may vary by
// OpenSSL build. Use hash_supported() to check before passing
// user-supplied names.
hash_hex(algo: string, data: string, length: int) -> {
out = cryptography_hash_hex_raw(algo, data, length)
if out == 0 {
if cryptography_hash_supported(algo) == 0 {
return "", "unknown algorithm"
}
return "", "openssl unavailable"
}
return out, ""
}
// Probe whether this build can compute `algo`. Returns 1 (yes) or 0
// (no). Always succeeds — never errors. Use this at config time to
// validate user-supplied algorithm names before they get written
// somewhere persistent.
hash_supported(algo: string) -> int {
return cryptography_hash_supported(algo)
}
// Base64 now lives in std.encoding (its correct home — it is an encoding,
// not cryptography). Use `encoding.base64_encode` / `_padded` / `_decode`.
// `random_base64` below still offers a crypto-random-bytes-to-base64
// convenience, now built on encoding.base64_encode.
// MD4 hex digest. Legacy — included for interop with formats that
// bake it into the wire shape (zsync's per-block strong checksum,
// some pre-2000 packet captures). NOT collision-resistant; do not
// use for security. Bound to EVP_md4() directly so it works on
// stock OpenSSL 3 (whose by-name dispatcher skips the legacy
// provider). Returns ("", "md4 unavailable") on builds whose
// OpenSSL was compiled without MD4 at all.
md4_hex(data: string, length: int) -> {
out = cryptography_md4_hex_raw(data, length)
if out == 0 {
return "", "md4 unavailable"
}
return out, ""
}
// MD5 hex digest. Legacy — Content-MD5, ETag, and various pre-SHA1
// fixtures. NOT collision-resistant; do not use for security.
md5_hex(data: string, length: int) -> {
out = cryptography_md5_hex_raw(data, length)
if out == 0 {
return "", "openssl unavailable"
}
return out, ""
}
// HMAC-SHA256 hex digest (RFC 2104 + FIPS 198-1, RFC 4231 test
// vectors). The hex form is the natural shape for opaque token
// signing and bearer-token derivation; for SigV4-style chained
// key derivation (each round's output keys the next), use the raw
// bytes form `hmac_sha256_bytes` so intermediate steps don't
// round-trip through hex.
// Copy a length-prefixed string payload (binary-safe, embedded NULs OK)
// into a fresh AetherBytes buffer for the pure-Aether HMAC.
hmac_str_to_bytes(s: string, n: int) -> ptr {
b = bytes.new(n)
i = 0
while i < n {
bytes.set(b, i, string_char_at_n(s, n, i))
i = i + 1
}
return b
}
hmac_sha256_hex(key: string, key_len: int, msg: string, msg_len: int) -> {
// Delegate to the PURE-AETHER HMAC (std.cryptography.hmac): it needs no
// libcrypto and produces a byte-identical digest, so HMAC works on every
// target — including cross builds without an OpenSSL sysroot. This closes
// the fail-open where the openssl-backed path stubbed to "" on cross,
// making a wrong/empty MAC compare equal in auth checks.
kb = hmac_str_to_bytes(key, key_len)
mb = hmac_str_to_bytes(msg, msg_len)
out = hmac.hmac_sha256_hex(kb, key_len, mb, msg_len)
bytes.free(kb)
bytes.free(mb)
if out == "" {
return "", "hmac failed"
}
return out, ""
}
// HMAC-SHA256 raw 32-byte digest. Returns (bytes, length, "")
// on success — `bytes` is an owned AetherString preserving every
// digest byte including embedded NULs. Use this for chained key
// derivation (SigV4, HKDF-shaped flows) where each round's output
// keys the next.
hmac_sha256_bytes(key: string, key_len: int, msg: string, msg_len: int) -> {
// Pure-Aether HMAC (see hmac_sha256_hex) — works on every target, no
// libcrypto. Returns the raw 32-byte digest as a length-preserving
// AetherString so embedded NULs survive (SigV4/HKDF chaining).
kb = hmac_str_to_bytes(key, key_len)
mb = hmac_str_to_bytes(msg, msg_len)
digest = hmac.hmac_sha256(kb, key_len, mb, msg_len)
bytes.free(kb)
bytes.free(mb)
if digest == null {
return "", 0, "hmac failed"
}
n = bytes.length(digest)
// bytes.finish hands the buffer off to a length-preserving AetherString
// (binary-safe, embedded NULs intact) and consumes the buffer.
owned = bytes.finish(digest, n)
return owned, n, ""
}
// Raw-bytes digest accessors. Same tuple shape as base64_decode and
// the HMAC-bytes form above. Use when the wire format expects a
// fixed-width digest (zsync's truncated 3–16 byte MD4 per block,
// binary git object IDs, etc.) — hex would double the size and
// lose the embedded-NUL semantics.
md4_bytes(data: string, length: int) -> {
ok = cryptography_md4_bytes_raw(data, length)
if ok == 0 {
return "", 0, "md4 unavailable"
}
raw = cryptography_get_binary_digest()
n = cryptography_get_binary_digest_length()
owned = string_new_with_length(raw, n)
cryptography_release_binary_digest()
return owned, n, ""
}
md5_bytes(data: string, length: int) -> {
ok = cryptography_md5_bytes_raw(data, length)
if ok == 0 {
return "", 0, "openssl unavailable"
}
raw = cryptography_get_binary_digest()
n = cryptography_get_binary_digest_length()
owned = string_new_with_length(raw, n)
cryptography_release_binary_digest()
return owned, n, ""
}
sha1_bytes(data: string, length: int) -> {
ok = cryptography_sha1_bytes_raw(data, length)
if ok == 0 {
return "", 0, "openssl unavailable"
}
raw = cryptography_get_binary_digest()
n = cryptography_get_binary_digest_length()
owned = string_new_with_length(raw, n)
cryptography_release_binary_digest()
return owned, n, ""
}
sha256_bytes(data: string, length: int) -> {
ok = cryptography_sha256_bytes_raw(data, length)
if ok == 0 {
return "", 0, "openssl unavailable"
}
raw = cryptography_get_binary_digest()
n = cryptography_get_binary_digest_length()
owned = string_new_with_length(raw, n)
cryptography_release_binary_digest()
return owned, n, ""
}
// Algorithm-by-name binary digest. Same dispatcher rules as
// hash_hex — useful when the algorithm is config-driven. Returns
// ("", 0, error) when:
// - OpenSSL isn't linked
// - the algorithm name is unknown ("unknown algorithm")
// - the digest computation fails
hash_bytes(algo: string, data: string, length: int) -> {
ok = cryptography_hash_bytes_raw(algo, data, length)
if ok == 0 {
if cryptography_hash_supported(algo) == 0 {
return "", 0, "unknown algorithm"
}
return "", 0, "openssl unavailable"
}
raw = cryptography_get_binary_digest()
n = cryptography_get_binary_digest_length()
owned = string_new_with_length(raw, n)
cryptography_release_binary_digest()
return owned, n, ""
}
// Draw `n` cryptographically-secure random bytes from the OS CSPRNG.
// Returns (bytes, length, "") on success — `bytes` is an owned
// AetherString preserving every byte including embedded NULs. Returns
// ("", 0, error) on failure (n < 0, allocation failure, syscall
// failure). The OS sources are:
// - Linux: getrandom(2) → /dev/urandom fallback
// - macOS/BSD: arc4random_buf(3)
// - Windows: BCryptGenRandom (CNG)
//
// std.math.random_int / random_float are SEEDABLE PRNGs (fine for
// sampling, NOT for secrets). When the value is unpredictability —
// bearer tokens, session keys, opaque storage filenames — this is the
// right primitive. n == 0 succeeds with a length-0 result.
random_bytes(n: int) -> {
ok = cryptography_random_bytes_raw(n)
if ok == 0 {
return "", 0, "random_bytes failed"
}
raw = cryptography_get_random_bytes()
out_n = cryptography_get_random_bytes_length()
owned = string_new_with_length(raw, out_n)
cryptography_release_random_bytes()
return owned, out_n, ""
}
// Draw `n` cryptographically-secure random bytes and return them as a
// lowercase-hex string (2*`n` chars). Returns ("", err) on failure.
// Composes random_bytes + per-byte hex emission; same OS-CSPRNG source
// and same caveat: NOT std.math. The motivating shape is opaque-bearer-
// token / API-key minting, where callers want a printable secret and
// don't want to hand-roll the byte→hex loop.
random_hex(n: int) -> {
raw, raw_n, err = random_bytes(n)
if err != "" {
return "", err
}
if n == 0 {
return "", ""
}
// Two ASCII chars per byte. Build into a bytes buffer to avoid
// O(n^2) string.concat; finish() hands off to a refcounted string.
out = bytes.new(raw_n * 2)
i = 0
while i < raw_n {
b = string.char_at_n(raw, raw_n, i)
hi = (b >> 4) & 15
lo = b & 15
c_hi = 48 + hi
if hi >= 10 {
c_hi = 87 + hi
}
c_lo = 48 + lo
if lo >= 10 {
c_lo = 87 + lo
}
bytes.set(out, i * 2, c_hi)
bytes.set(out, i * 2 + 1, c_lo)
i = i + 1
}
return bytes.finish(out, raw_n * 2), ""
}
// Draw `n` cryptographically-secure random bytes and return them as
// RFC 4648 §4 standard-alphabet Base64 (unpadded — same shape as
// base64_encode). Returns ("", err) on failure. Useful when the wire
// format prefers a denser printable encoding than hex (~33% shorter).
// n == 0 yields "".
random_base64(n: int) -> {
raw, raw_n, err = random_bytes(n)
if err != "" {
return "", err
}
if n == 0 {
return "", ""
}
return encoding.base64_encode(raw, raw_n), ""
}
// ---- Incremental (streaming) digest context ----
//
// Hash data that arrives in pieces — without ever holding it whole.
// The motivating shape is a blob store streaming an upload to disk in
// fixed windows: open a context, feed each window as it lands, then
// finalize to get the ETag, instead of reading the stored object back
// off disk purely to MD5 it. S3 ETag = md5-of-the-object; multipart
// ETag = md5-of-md5s — both want a context that survives across reads.
//
// ctx, err = cryptography.digest_new("md5")
// // inside the window loop:
// ok, uerr = cryptography.digest_update(ctx, chunk, n)
// // after the loop:
// etag, ferr = cryptography.digest_final_hex(ctx) // ctx now freed
//
// The two final() variants free the context, so the success path needs
// no explicit free; call digest_free only when bailing out before
// finalizing (e.g. an aborted upload).
// Create a streaming digest context for `algo` ("md5", "sha256",
// "sha1", "md4", or any name OpenSSL's by-name lookup recognizes —
// same rules as hash_hex). Returns (ctx, "") on success, (null, error)
// on unknown algorithm / OOM / no OpenSSL. The returned handle is
// opaque; thread it through digest_update / digest_final_*.
digest_new(algo: string) -> {
ctx = cryptography_digest_new_raw(algo)
if ctx == null {
if cryptography_hash_supported(algo) == 0 {
return null, "unknown algorithm"
}
return null, "openssl unavailable"
}
return ctx, ""
}
// Feed `length` bytes of `data` into the context. Binary-safe (`data`
// may be an owned AetherString from fs.pread / a window buffer; embedded
// NULs survive). Returns (1, "") on success, (0, error) on failure.
// Feeding 0 bytes is a no-op success. Does NOT free the context.
digest_update(ctx: ptr, data: string, length: int) -> {
ok = cryptography_digest_update_raw(ctx, data, length)
if ok == 0 {
return 0, "digest update failed"
}
return 1, ""
}
// Finalize the context and return the digest as lowercase hex (32 chars
// for md5/md4, 40 for sha1, 64 for sha256). The context is FREED by this
// call — do not reuse or free it again. Returns (hex, "") on success,
// ("", error) on failure.
digest_final_hex(ctx: ptr) -> {
out = cryptography_digest_final_hex_raw(ctx)
if out == 0 {
return "", "digest finalize failed"
}
return out, ""
}
// Finalize the context and return the raw digest bytes as an owned
// AetherString (16 bytes for md5/md4, 20 for sha1, 32 for sha256).
// The context is FREED by this call. Use this when the raw bytes key a
// subsequent step (SigV4 payload hashing). Returns (bytes, length, "")
// on success, ("", 0, error) on failure.
digest_final_bytes(ctx: ptr) -> {
ok = cryptography_digest_final_bytes_raw(ctx)
if ok == 0 {
return "", 0, "digest finalize failed"
}
raw = cryptography_get_binary_digest()
n = cryptography_get_binary_digest_length()
owned = string_new_with_length(raw, n)
cryptography_release_binary_digest()
return owned, n, ""
}
// Abandon a context without finalizing — the cleanup path for an
// aborted operation. NULL-safe; a no-op on a null handle. After a
// successful digest_final_* the context is already freed, so this is
// only for the bail-out-before-final case.
digest_free(ctx: ptr) {
cryptography_digest_free_raw(ctx)
}