/* node:crypto Cipheriv and Decipheriv over the vendored mbedTLS block * ciphers: aes-128/192/256 in CBC, CTR and GCM modes and * chacha20-poly1305. A cipher is a native dyn handle; its update(), * final(), setAAD(), setAutoPadding(), getAuthTag() and setAuthTag() * methods follow Node 24's lib/internal/crypto/cipher.js argument handling * and src/crypto/crypto_cipher.cc state machine. CBC buffering and PKCS#7 * padding follow OpenSSL 3's generic block provider, so every update() * returns the same number of bytes as Node's. String output goes through * the runtime StringDecoder, which carries partial characters and base64 * groups between calls as Node's does. chacha20-poly1305 follows OpenSSL's * construction, which also accepts AAD after data. Authentication tags are * compared in constant time. A cipher used as a stream reads and writes * through its own native Transform, which the stream unit builds on first * use, as Node's LazyTransform is. All arguments are borrowed. */ #include "scr_cipher_names.h" #include "scr_runtime.h" #include #include #include #include "mbedtls/aes.h" #include "mbedtls/chacha20.h" #include "mbedtls/des.h" #include "mbedtls/gcm.h" #include "mbedtls/poly1305.h" #include "mbedtls/platform_util.h" /* SC_CHACHA is chacha20-poly1305; SC_CHACHA20 the bare stream cipher. */ typedef enum { SC_CBC, SC_CTR, SC_GCM, SC_CHACHA, SC_ECB, SC_CFB, SC_CFB8, SC_CFB1, SC_OFB, SC_CHACHA20, /* CCM and the key wraps stay last: sc_transform tests mode >= SC_CCM. */ SC_CCM, SC_WRAP, /* RFC 3394 AES key wrap */ SC_WRAP_PAD, /* RFC 5649 AES key wrap with padding */ } ScCipherMode; typedef struct { const char *name; ScCipherMode mode; unsigned key_len; bool des; /* triple DES (two- or three-key) rather than AES */ bool inverse; /* key wrap with the inverse cipher (the -inv names) */ } ScCipherSpec; /* Node's names for the implemented ciphers, including OpenSSL's aliases. * Lookup is case-insensitive, like OpenSSL's, and scans in order, so the * CCM and key wrap names follow the others. */ static const ScCipherSpec sc_specs[] = { {"aes-128-cbc", SC_CBC, 16}, {"aes-192-cbc", SC_CBC, 24}, {"aes-256-cbc", SC_CBC, 32}, {"aes128", SC_CBC, 16}, {"aes192", SC_CBC, 24}, {"aes256", SC_CBC, 32}, {"aes-128-ctr", SC_CTR, 16}, {"aes-192-ctr", SC_CTR, 24}, {"aes-256-ctr", SC_CTR, 32}, {"aes-128-gcm", SC_GCM, 16}, {"aes-192-gcm", SC_GCM, 24}, {"aes-256-gcm", SC_GCM, 32}, {"id-aes128-gcm", SC_GCM, 16}, {"id-aes192-gcm", SC_GCM, 24}, {"id-aes256-gcm", SC_GCM, 32}, {"chacha20-poly1305", SC_CHACHA, 32}, {"aes-128-ecb", SC_ECB, 16}, {"aes-192-ecb", SC_ECB, 24}, {"aes-256-ecb", SC_ECB, 32}, {"aes-128-cfb", SC_CFB, 16}, {"aes-192-cfb", SC_CFB, 24}, {"aes-256-cfb", SC_CFB, 32}, {"aes-128-cfb8", SC_CFB8, 16}, {"aes-192-cfb8", SC_CFB8, 24}, {"aes-256-cfb8", SC_CFB8, 32}, {"aes-128-cfb1", SC_CFB1, 16}, {"aes-192-cfb1", SC_CFB1, 24}, {"aes-256-cfb1", SC_CFB1, 32}, {"aes-128-ofb", SC_OFB, 16}, {"aes-192-ofb", SC_OFB, 24}, {"aes-256-ofb", SC_OFB, 32}, {"des-ede3-cbc", SC_CBC, 24, true}, {"des3", SC_CBC, 24, true}, {"des-ede3", SC_ECB, 24, true}, {"des-ede3-ecb", SC_ECB, 24, true}, {"des-ede3-cfb", SC_CFB, 24, true}, {"des-ede3-cfb8", SC_CFB8, 24, true}, {"des-ede3-cfb1", SC_CFB1, 24, true}, {"des-ede3-ofb", SC_OFB, 24, true}, {"des-ede-cbc", SC_CBC, 16, true}, {"des-ede", SC_ECB, 16, true}, {"des-ede-ecb", SC_ECB, 16, true}, {"des-ede-cfb", SC_CFB, 16, true}, {"des-ede-ofb", SC_OFB, 16, true}, {"chacha20", SC_CHACHA20, 32}, {"aes-128-ccm", SC_CCM, 16}, {"aes-192-ccm", SC_CCM, 24}, {"aes-256-ccm", SC_CCM, 32}, {"id-aes128-ccm", SC_CCM, 16}, {"id-aes192-ccm", SC_CCM, 24}, {"id-aes256-ccm", SC_CCM, 32}, {"aes-128-wrap", SC_WRAP, 16, false, false}, {"aes128-wrap", SC_WRAP, 16, false, false}, {"id-aes128-wrap", SC_WRAP, 16, false, false}, {"aes-128-wrap-inv", SC_WRAP, 16, false, true}, {"aes128-wrap-inv", SC_WRAP, 16, false, true}, {"aes-128-wrap-pad", SC_WRAP_PAD, 16, false, false}, {"aes128-wrap-pad", SC_WRAP_PAD, 16, false, false}, {"id-aes128-wrap-pad", SC_WRAP_PAD, 16, false, false}, {"aes-128-wrap-pad-inv", SC_WRAP_PAD, 16, false, true}, {"aes128-wrap-pad-inv", SC_WRAP_PAD, 16, false, true}, {"aes-192-wrap", SC_WRAP, 24, false, false}, {"aes192-wrap", SC_WRAP, 24, false, false}, {"id-aes192-wrap", SC_WRAP, 24, false, false}, {"aes-192-wrap-inv", SC_WRAP, 24, false, true}, {"aes192-wrap-inv", SC_WRAP, 24, false, true}, {"aes-192-wrap-pad", SC_WRAP_PAD, 24, false, false}, {"aes192-wrap-pad", SC_WRAP_PAD, 24, false, false}, {"id-aes192-wrap-pad", SC_WRAP_PAD, 24, false, false}, {"aes-192-wrap-pad-inv", SC_WRAP_PAD, 24, false, true}, {"aes192-wrap-pad-inv", SC_WRAP_PAD, 24, false, true}, {"aes-256-wrap", SC_WRAP, 32, false, false}, {"aes256-wrap", SC_WRAP, 32, false, false}, {"id-aes256-wrap", SC_WRAP, 32, false, false}, {"aes-256-wrap-inv", SC_WRAP, 32, false, true}, {"aes256-wrap-inv", SC_WRAP, 32, false, true}, {"aes-256-wrap-pad", SC_WRAP_PAD, 32, false, false}, {"aes256-wrap-pad", SC_WRAP_PAD, 32, false, false}, {"id-aes256-wrap-pad", SC_WRAP_PAD, 32, false, false}, {"aes-256-wrap-pad-inv", SC_WRAP_PAD, 32, false, true}, {"aes256-wrap-pad-inv", SC_WRAP_PAD, 32, false, true}, }; /* Ciphers OpenSSL 3 keeps in its legacy provider, which Node 24 does not * load: crypto.getCipherInfo() knows them, and createCipheriv checks their * IV length, then fails with OpenSSL's "unsupported" error. */ static const struct { const char *name; unsigned iv_len; } sc_legacy[] = {SCR_CIPHER_LEGACY_ENTRIES}; /* crypto.getCiphers() on Node 24.22 (OpenSSL 3.5). Names in this list that * have no implementation throw a coded SC2020 error instead of Node's * ERR_CRYPTO_UNKNOWN_CIPHER. */ static const char *const sc_known[] = {SCR_CIPHER_KNOWN_NAMES}; enum { SC_NO_TAG = -1 }; enum { SC_TAG_UNKNOWN, SC_TAG_SET_BY_USER, SC_TAG_COMPUTED }; typedef struct ScCipher { size_t rc; ScCipherMode mode; bool decrypt; bool active; /* false after final() or a failed backend call */ bool pad; /* CBC and ECB automatic padding */ bool des; /* triple DES rather than AES */ unsigned bs; /* the block size: 16 for AES, 8 for triple DES */ bool data_started; /* a non-empty update() ran, so non-empty AAD is refused */ int tag_len; /* SC_NO_TAG until known */ int tag_state; unsigned char tag[16]; unsigned char block[16]; /* CBC: chaining value; CTR: counter block; CFB, OFB: shift register; chacha20, CCM: the IV; key wrap: the integrity check value */ unsigned char buf[16]; /* CBC, ECB: buffered input; CTR: keystream block */ size_t bufsz; /* CBC, ECB: buffered bytes; CTR, CFB, OFB: keystream offset */ uint64_t chacha_left; /* chacha20: bytes before the low counter word wraps */ mbedtls_aes_context aes; mbedtls_des3_context des3; mbedtls_gcm_context gcm; mbedtls_chacha20_context chacha; mbedtls_poly1305_context poly; uint64_t aad_len; /* chacha20-poly1305 AAD bytes */ uint64_t text_len; /* chacha20-poly1305 data bytes */ bool aad_open; /* AAD was added since the last data or padding */ /* CCM: OpenSSL's ccm128 state (the counter block, whose first byte holds * the L and M parameters and the Adata flag, and the CBC-MAC), its * provider's flags, and Node's one-update rule. A decipher's expected * tag is `tag`. */ size_t ccm_iv_len; unsigned char ccm_nonce[16]; unsigned char ccm_cmac[16]; bool ccm_iv_set, ccm_len_set, ccm_tag_set; bool ccm_updated; /* a decipher's one update() ran; key wrap: the one update() */ bool ccm_auth_failed; /* that update failed to authenticate */ bool ccm_verified; /* a decipher's tag matched the one it computed */ double ccm_max_message; bool wrap_decrypt_block; /* key wrap: the AES key schedule decrypts */ ScrStr *decoder; /* the StringDecoder encoding once string output starts */ double pending; /* the decoder's carried bytes */ /* The Transform of a cipher used as a stream (+1) and its release * (scr_stream.c). Its steps hold the cipher, so the cipher is a * collector-headered object behind a traced handle, and the pair is * reclaimed as a cycle. */ void *stream; void (*stream_release)(void *); } ScCipher; static void *sc_retain(void *h) { ((ScCipher *)h)->rc++; return h; } static void sc_trace(void *h, ScrTraceVisit visit, void *ctx) { ScCipher *c = h; if (c->stream) visit(c->stream, ctx); } /* The collector frees the stream itself; the rest is released here. */ static void sc_gcfree(void *h) { ScCipher *c = h; mbedtls_aes_free(&c->aes); mbedtls_des3_free(&c->des3); mbedtls_gcm_free(&c->gcm); mbedtls_chacha20_free(&c->chacha); mbedtls_poly1305_free(&c->poly); scr_str_release(c->decoder); mbedtls_platform_zeroize(c, sizeof *c); scr_obj_free_note(); scr_cyc_free(c); } static void sc_destroy(void *h) { ScCipher *c = h; void *stream = c->stream; c->stream = NULL; if (stream) c->stream_release(stream); sc_gcfree(c); } static void sc_release(void *h) { ScCipher *c = h; if (--c->rc == 0) { scr_cyc_dead(c); scr_rc_destroy(c, sc_destroy); } else scr_cyc_released(c); } /* ── errors ───────────────────────────────────────────────────────────── */ static void sc_throw(int kind, const char *code, const char *message) { if (code) scr_throw_error_msg_code(kind, message, strlen(message), code); else scr_throw_error_msg(kind, message, strlen(message)); } static void sc_invalid_state(const char *operation) { char message[64]; snprintf(message, sizeof message, "Invalid state for operation %s", operation); sc_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INVALID_STATE", message); } /* A backend call failed. The context is retired, like Node's after * final(): later calls throw, and no output or tag is returned. */ static void sc_fail(ScCipher *c) { c->active = false; c->tag_state = SC_TAG_UNKNOWN; mbedtls_platform_zeroize(c->tag, sizeof c->tag); mbedtls_platform_zeroize(c->buf, sizeof c->buf); c->bufsz = 0; } static void sc_invalid_tag(unsigned long length) { char message[80]; snprintf(message, sizeof message, "Invalid authentication tag length: %lu", length); sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_AUTH_TAG", message); } /* An OpenSSL provider error as Node reports it: the formatted message plus * own opensslErrorStack, library, reason and code properties, in Node's * key order. */ /* OpenSSL's error with its library, reason and error stack; Node's * code. */ static void sc_throw_openssl_in(const char *packed, const char *library, const char *reason, const char *code) { char message[128]; snprintf(message, sizeof message, "error:%s:%s::%s", packed, library, reason); ScrStr *text = scr_str_new(message, strlen(message)); ScrError *e = scr_error_new(SCR_ERR_ERROR, text); ScrDyn *stack = scr_dyn_new_arr(); scr_dyn_arr_push(stack, scr_dyn_new_str(text)); scr_str_release(text); scr_error_set_prop(e, "opensslErrorStack", 17, stack); ScrStr *library_text = scr_str_new(library, strlen(library)); scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library_text)); scr_str_release(library_text); ScrStr *reason_text = scr_str_new(reason, strlen(reason)); scr_error_set_prop(e, "reason", 6, scr_dyn_new_str(reason_text)); scr_str_release(reason_text); scr_error_set_code(e, code); ScrStr *code_text = scr_str_new(code, strlen(code)); scr_error_set_prop(e, "code", 4, scr_dyn_new_str(code_text)); scr_str_release(code_text); scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); } static void sc_throw_openssl(const char *packed, const char *reason, const char *code) { sc_throw_openssl_in(packed, "Provider routines", reason, code); } /* ERR_INVALID_ARG_VALUE's property flavor: "The property 'options.x' is * invalid. Received ". */ static void sc_invalid_property(const char *name, const ScrDyn *value) { char rendered[160]; const char *text = scr_dyn_inspect_lite(value, rendered, sizeof rendered); ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "The property '"); scr_jb_puts(&b, name); scr_jb_puts(&b, "' is invalid. Received "); scr_jb_puts(&b, text); ScrStr *message = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_INVALID_ARG_VALUE"); scr_str_release(message); } /* validateOneOf: "The property 'options.x' must be one of: 'A', 'B'. * Received ". */ static void sc_not_one_of(const char *name, const char *allowed, const ScrDyn *value) { char rendered[160]; const char *text = scr_dyn_inspect_lite(value, rendered, sizeof rendered); ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "The property '"); scr_jb_puts(&b, name); scr_jb_puts(&b, "' must be one of: "); scr_jb_puts(&b, allowed); scr_jb_puts(&b, ". Received "); scr_jb_puts(&b, text); ScrStr *message = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_INVALID_ARG_VALUE"); scr_str_release(message); } /* An object's toString the program defined, as an own or inherited * function property. */ static bool sc_custom_to_string(ScrDyn *value) { if (value->kind != SCR_DYN_OBJ && value->kind != SCR_DYN_TYPED_REF) return false; ScrDyn *member = scr_dyn_obj_read(value, "toString", 8); bool custom = member && member->kind == SCR_DYN_FUNC; scr_dyn_release(member); return custom; } /* Whether value is a class instance whose class defines toString. */ static bool sc_class_to_string(const ScrDyn *value) { ScrStr *name = scr_str_new("toString", 8); bool method = scr_dyn_typed_ref_class_has(value, name); scr_str_release(name); return method; } /* ERR_UNKNOWN_ENCODING formats the encoding with %s: objects without a * program-defined toString are inspected at depth 0. */ static void sc_unknown_encoding(const ScrDyn *encoding) { ScrDyn *value = (ScrDyn *)encoding; ScrStr *text; /* Node prints a function's source text and calls a class's toString. */ if (value->kind == SCR_DYN_FUNC || sc_class_to_string(value)) { static const char message[] = "An output encoding that is a function or an instance of a class defining " "toString has no lowering; pass the encoding name as a string"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return; } if (value->kind == SCR_DYN_STR || sc_custom_to_string(value)) { text = scr_dyn_string_coerce_js(value); } else if (value->kind == SCR_DYN_NUM) { text = scr_insp_f64(value->v.num); } else { text = scr_insp_dyn_s(value, 0); } if (!text) return; ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "Unknown encoding: "); scr_jb_put_str(&b, text); scr_str_release(text); ScrStr *message = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_UNKNOWN_ENCODING"); scr_str_release(message); } /* ── argument conversion ──────────────────────────────────────────────── */ static bool sc_ascii_eq_ci(const char *a, size_t a_len, const char *b) { size_t b_len = strlen(b); if (a_len != b_len) return false; for (size_t i = 0; i < a_len; i++) { char x = a[i], y = b[i]; if (x >= 'A' && x <= 'Z') x = (char)(x - 'A' + 'a'); if (x != y) return false; } return true; } static bool sc_str_is(const ScrStr *text, const char *literal) { size_t len = strlen(literal); return text->len == len && memcmp(text->data, literal, len) == 0; } /* Node's normalizeEncoding: the canonical name, or NULL. */ static const char *sc_normalize_encoding(const ScrStr *name) { static const struct { const char *from; const char *to; } map[] = { {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"}, {"base64", "base64"}, {"base64url", "base64url"}, {"latin1", "latin1"}, {"binary", "latin1"}, {"ascii", "ascii"}, {"utf16le", "utf16le"}, {"utf-16le", "utf16le"}, {"ucs2", "utf16le"}, {"ucs-2", "utf16le"}, }; for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) if (sc_ascii_eq_ci(name->data, name->len, map[i].from)) return map[i].to; return NULL; } static bool sc_is_view(const ScrDyn *value) { return value->kind == SCR_DYN_BYTES; } /* A u8 view of an ArrayBuffer, Buffer, TypedArray or DataView (+1). */ static ScrBytes *sc_bytes_of(const ScrDyn *value) { return scr_bytes_buffer_source(value); } /* Buffer.from(string, encoding) with Node's encoding rules: a missing or * non-string encoding reads as utf8, an unknown name throws * ERR_UNKNOWN_ENCODING. "buffer" reads as utf8. +1 or NULL. */ static ScrBytes *sc_string_bytes(const ScrStr *text, const ScrDyn *encoding) { const char *name = "utf8"; if (encoding && encoding->kind == SCR_DYN_STR && encoding->v.str->len > 0 && !sc_ascii_eq_ci(encoding->v.str->data, encoding->v.str->len, "buffer")) { name = sc_normalize_encoding(encoding->v.str); if (!name) { sc_unknown_encoding(encoding); return NULL; } } ScrStr *canonical = scr_str_new(name, strlen(name)); ScrBytes *bytes = scr_bytes_from_str(text, canonical); scr_str_release(canonical); return bytes; } /* getArrayBufferOrView(value, name, encoding). +1 or NULL. */ static ScrBytes *sc_buffer_or_view(const ScrDyn *value, const char *name, const ScrDyn *encoding) { if (value->kind == SCR_DYN_STR) return sc_string_bytes(value->v.str, encoding); if (sc_is_view(value) || scr_buffer_storage_is(value)) return sc_bytes_of(value); scr_dyn_arg_type_fail(name, "of type string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView", value); return NULL; } /* The value a typed-ref box holds when it is a string, view, ArrayBuffer * or native handle such as a KeyObject (+1). Other values are retained * as passed, so argument errors describe the original value. NULL after * throwing. */ static ScrDyn *sc_unbox(ScrDyn *value) { if (value->kind != SCR_DYN_TYPED_REF) return scr_dyn_retain(value); ScrDyn *view = scr_dyn_typed_ref_materialize(value); if (!view) return NULL; if (view->kind == SCR_DYN_STR || sc_is_view(view) || view->kind == SCR_DYN_HANDLE || scr_buffer_storage_is(view)) return view; scr_dyn_release(view); return scr_dyn_retain(value); } /* options[key] when options is an object, else undefined (+1 or NULL). */ static ScrDyn *sc_option(const ScrDyn *options, const char *key) { if (options->kind == SCR_DYN_OBJ || options->kind == SCR_DYN_TYPED_REF || options->kind == SCR_DYN_PROXY) { ScrDyn *view = options->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(options) : scr_dyn_retain((ScrDyn *)options); if (scr_exc_pending()) { scr_dyn_release(view); return NULL; } ScrDyn *value = scr_dyn_obj_read(view, key, strlen(key)); scr_dyn_release(view); if (!value && !scr_exc_pending()) value = scr_dyn_retain(scr_dyn_undefined()); return value; } return scr_dyn_retain(scr_dyn_undefined()); } /* getStringOption: the option when it is a string, NULL for nullish, and * ERR_INVALID_ARG_TYPE otherwise. *failed reports a pending exception. */ static ScrDyn *sc_string_option(const ScrDyn *options, const char *key, bool *failed) { *failed = false; ScrDyn *value = sc_option(options, key); if (!value) { *failed = true; return NULL; } if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) { scr_dyn_release(value); return NULL; } if (value->kind != SCR_DYN_STR) { char name[48]; snprintf(name, sizeof name, "options.%s", key); scr_dyn_prop_type_fail(name, "of type string", value); scr_dyn_release(value); *failed = true; return NULL; } return value; } /* getUIntOption: the uint32 value, -1 when nullish, -2 after throwing * ERR_INVALID_ARG_VALUE. */ static double sc_uint_option(const ScrDyn *options, const char *key) { if (options->kind == SCR_DYN_UNDEF || options->kind == SCR_DYN_NULL || !scr_dyn_truthy(options)) return -1; ScrDyn *value = sc_option(options, key); if (!value) return -2; double result = -1; if (value->kind != SCR_DYN_UNDEF && value->kind != SCR_DYN_NULL) { /* value >>> 0 === value: -0 passes as 0. */ if (value->kind == SCR_DYN_NUM && value->v.num >= 0 && value->v.num <= 4294967295.0 && value->v.num == (double)(uint32_t)value->v.num) { result = value->v.num + 0.0; } else { char name[48]; snprintf(name, sizeof name, "options.%s", key); sc_invalid_property(name, value); result = -2; } } scr_dyn_release(value); return result; } /* ── construction ─────────────────────────────────────────────────────── */ static const ScCipherSpec *sc_lookup(const ScrStr *name) { for (size_t i = 0; i < sizeof sc_specs / sizeof sc_specs[0]; i++) if (sc_ascii_eq_ci(name->data, name->len, sc_specs[i].name)) return &sc_specs[i]; return NULL; } /* The legacy-provider IV length of `name`, or -1 for other names. */ static int sc_legacy_iv(const ScrStr *name) { for (size_t i = 0; i < sizeof sc_legacy / sizeof sc_legacy[0]; i++) if (sc_ascii_eq_ci(name->data, name->len, sc_legacy[i].name)) return (int)sc_legacy[i].iv_len; return -1; } /* The IV length a mode takes; SC_GCM takes any nonzero length. */ static size_t sc_iv_length(ScCipherMode mode, bool des) { switch (mode) { case SC_ECB: return 0; case SC_CHACHA: return 12; case SC_CHACHA20: return 16; case SC_WRAP: return 8; case SC_WRAP_PAD: return 4; default: return des ? 8 : 16; } } static bool sc_known_name(const ScrStr *name) { for (size_t i = 0; i < sizeof sc_known / sizeof sc_known[0]; i++) if (sc_ascii_eq_ci(name->data, name->len, sc_known[i])) return true; return false; } static void sc_unsupported_cipher(const ScrStr *name) { ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "The cipher '"); scr_jb_put_str(&b, name); scr_jb_puts(&b, "' has no native implementation; the supported ciphers are aes-128, aes-192 and " "aes-256 in cbc, ctr, gcm, ccm, ecb, cfb, cfb8, cfb1 and ofb modes and their wrap, " "wrap-pad, wrap-inv and wrap-pad-inv key wraps, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 " "and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and chacha20-poly1305"); ScrStr *message = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); scr_str_release(message); } static bool sc_valid_gcm_tag(unsigned long length) { return length == 4 || length == 8 || (length >= 12 && length <= 16); } static ScrDynHandleOps sc_ops_cipher, sc_ops_decipher; static ScrDyn *sc_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what); static ScrDyn *sc_get(void *h, const char *key, size_t key_len); static bool sc_set(void *h, const char *key, size_t key_len, const ScrDyn *value); /* createCipheriv/createDecipheriv(cipher, key, iv, options): the argument * checks of createCipherWithIV and the cipher, IV, tag-length and key * checks of CipherBase::InitIv, in Node's order. +1 handle or NULL. */ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn *iv, ScrDyn *options) { if (cipher->kind != SCR_DYN_STR) { scr_dyn_arg_type_fail("cipher", "of type string", cipher); return NULL; } bool failed; ScrDyn *encoding = sc_string_option(options, "encoding", &failed); if (failed) return NULL; ScrBytes *key_bytes = NULL; size_t key_object_len; const unsigned char *key_object = scr_key_object_bytes(key, &key_object_len); if (key_object) { /* prepareSecretKey: a secret KeyObject supplies its bytes. */ key_bytes = scr_bytes_new(SCR_BYTES_U8, (double)key_object_len); if (key_object_len) memcpy(key_bytes->data, key_object, key_object_len); } else if (key->kind == SCR_DYN_STR || sc_is_view(key) || scr_buffer_storage_is(key)) { key_bytes = sc_buffer_or_view(key, "key", encoding); } else { scr_dyn_arg_type_fail("key", "of type string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, KeyObject, or CryptoKey", key); } scr_dyn_release(encoding); if (!key_bytes) return NULL; ScrBytes *iv_bytes = NULL; if (iv->kind != SCR_DYN_NULL) { iv_bytes = sc_buffer_or_view(iv, "iv", NULL); if (!iv_bytes) { scr_bytes_release(key_bytes); return NULL; } } double tag_option = sc_uint_option(options, "authTagLength"); ScrDyn *result = NULL; ScrDyn *cts_mode = NULL, *xts_standard = NULL; if (tag_option == -2) goto done; cts_mode = sc_string_option(options, "ctsMode", &failed); if (failed) goto done; xts_standard = sc_string_option(options, "xtsStandard", &failed); if (failed) goto done; if (cts_mode && !sc_str_is(cts_mode->v.str, "CS1") && !sc_str_is(cts_mode->v.str, "CS2") && !sc_str_is(cts_mode->v.str, "CS3")) { sc_not_one_of("options.ctsMode", "'CS1', 'CS2', 'CS3'", cts_mode); goto done; } if (xts_standard && !sc_str_is(xts_standard->v.str, "GB") && !sc_str_is(xts_standard->v.str, "IEEE")) { sc_not_one_of("options.xtsStandard", "'GB', 'IEEE'", xts_standard); goto done; } const ScCipherSpec *spec = sc_lookup(cipher->v.str); size_t iv_len = iv_bytes ? iv_bytes->len : 0; if (!spec) { int legacy_iv = sc_legacy_iv(cipher->v.str); if (legacy_iv >= 0) { if (iv_len != (size_t)legacy_iv || (legacy_iv > 0 && !iv_bytes)) sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_IV", "Invalid initialization vector"); else sc_throw_openssl_in("0308010C", "digital envelope routines", "unsupported", "ERR_OSSL_EVP_UNSUPPORTED"); } else if (sc_known_name(cipher->v.str)) { sc_unsupported_cipher(cipher->v.str); } else { sc_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNKNOWN_CIPHER", "Unknown cipher"); } goto done; } size_t expected_iv = sc_iv_length(spec->mode, spec->des); bool iv_ok = spec->mode == SC_GCM ? iv_len > 0 : spec->mode == SC_CCM ? iv_len >= 7 && iv_len <= 13 : iv_len == expected_iv && (expected_iv == 0 || iv_bytes); if (!iv_ok) { sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_IV", "Invalid initialization vector"); goto done; } /* None of the implemented ciphers is a CTS or XTS mode. */ if (cts_mode || xts_standard) { ScrJsonBuf b; scr_jb_init(&b); scr_jb_put_str(&b, cipher->v.str); scr_jb_puts(&b, cts_mode ? " does not support the ctsMode option" : " does not support the xtsStandard option"); ScrStr *message = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "ERR_CRYPTO_UNSUPPORTED_OPERATION"); scr_str_release(message); goto done; } int tag_len = SC_NO_TAG; if (spec->mode == SC_GCM && tag_option >= 0) { if (!sc_valid_gcm_tag((unsigned long)tag_option)) { sc_invalid_tag((unsigned long)tag_option); goto done; } tag_len = (int)tag_option; } else if (spec->mode == SC_CHACHA) { tag_len = tag_option < 0 ? 16 : (int)(tag_option > 16 ? 17 : tag_option); if (tag_len < 1 || tag_len > 16) { sc_invalid_tag((unsigned long)tag_option); goto done; } } else if (spec->mode == SC_CCM) { /* InitAuthenticated: CCM needs the tag length, which OpenSSL takes when * it is even and 4 to 16 bytes. */ if (tag_option < 0) { ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "authTagLength required for "); scr_jb_put_str(&b, cipher->v.str); ScrStr *message = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_CRYPTO_INVALID_AUTH_TAG"); scr_str_release(message); goto done; } if (tag_option < 4 || tag_option > 16 || (unsigned long)tag_option % 2 != 0) { sc_invalid_tag((unsigned long)tag_option); goto done; } tag_len = (int)tag_option; } if (key_bytes->len != spec->key_len) { sc_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Invalid key length"); goto done; } static ScrCycType cyc_type = {&sc_trace, &sc_gcfree, SCR_CYC_KIND_NONE, 0}; ScCipher *c = scr_cyc_alloc(sizeof *c, &cyc_type); c->rc = 1; scr_obj_alloc_note(); c->mode = spec->mode; c->decrypt = decrypt; c->active = true; c->pad = true; c->des = spec->des; c->bs = spec->des ? 8 : 16; c->tag_len = tag_len; mbedtls_aes_init(&c->aes); mbedtls_des3_init(&c->des3); mbedtls_gcm_init(&c->gcm); mbedtls_chacha20_init(&c->chacha); mbedtls_poly1305_init(&c->poly); int rc = 0; unsigned bits = spec->key_len * 8; switch (spec->mode) { case SC_CBC: case SC_ECB: case SC_CFB: case SC_CFB8: case SC_CFB1: case SC_OFB: { /* CBC and ECB decrypt with the inverse cipher; the feedback modes run * the forward cipher both ways. */ bool inverse = decrypt && (spec->mode == SC_CBC || spec->mode == SC_ECB); if (spec->des && spec->key_len == 24) rc = inverse ? mbedtls_des3_set3key_dec(&c->des3, key_bytes->data) : mbedtls_des3_set3key_enc(&c->des3, key_bytes->data); else if (spec->des) rc = inverse ? mbedtls_des3_set2key_dec(&c->des3, key_bytes->data) : mbedtls_des3_set2key_enc(&c->des3, key_bytes->data); else rc = inverse ? mbedtls_aes_setkey_dec(&c->aes, key_bytes->data, bits) : mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); if (expected_iv) memcpy(c->block, iv_bytes->data, expected_iv); break; } case SC_CHACHA20: /* OpenSSL's IV is a 64-bit little-endian block counter and an 8-byte * nonce; mbedTLS takes a 32-bit counter and a 12-byte nonce, so the * stream restarts each time the low counter word wraps. */ memcpy(c->block, iv_bytes->data, 16); rc = mbedtls_chacha20_setkey(&c->chacha, key_bytes->data); if (rc == 0) { uint32_t low = (uint32_t)c->block[0] | (uint32_t)c->block[1] << 8 | (uint32_t)c->block[2] << 16 | (uint32_t)c->block[3] << 24; c->chacha_left = ((uint64_t)1 << 32) - low; c->chacha_left *= 64; rc = mbedtls_chacha20_starts(&c->chacha, c->block + 4, low); } break; case SC_CTR: rc = mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); memcpy(c->block, iv_bytes->data, 16); break; case SC_WRAP: case SC_WRAP_PAD: /* SP 800-38F: wrapping is the forward transformation (encryption) and * unwrapping the inverse, and the -inv ciphers swap the two. The IV is * the wrap's integrity check value. */ rc = decrypt != spec->inverse ? mbedtls_aes_setkey_dec(&c->aes, key_bytes->data, bits) : mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); c->wrap_decrypt_block = decrypt != spec->inverse; memcpy(c->block, iv_bytes->data, iv_len); break; case SC_CCM: { /* CRYPTO_ccm128_init with M = the tag length and L = 15 - the IV * length; Node caps the message at what a 12- or 13-byte IV's length * field holds, and at INT_MAX otherwise. */ rc = mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); unsigned L = 15 - (unsigned)iv_len; c->ccm_nonce[0] = (unsigned char)(((L - 1) & 7) | ((((unsigned)tag_len - 2) / 2) & 7) << 3); memcpy(c->block, iv_bytes->data, iv_len); c->ccm_iv_len = iv_len; c->ccm_iv_set = true; c->ccm_max_message = iv_len == 13 ? 65535 : iv_len == 12 ? 16777215 : 2147483647; break; } case SC_GCM: rc = mbedtls_gcm_setkey(&c->gcm, MBEDTLS_CIPHER_ID_AES, key_bytes->data, bits); if (rc == 0) rc = mbedtls_gcm_starts(&c->gcm, decrypt ? MBEDTLS_GCM_DECRYPT : MBEDTLS_GCM_ENCRYPT, iv_bytes->data, iv_len); break; case SC_CHACHA: { /* RFC 8439: block 0 of the keystream is the Poly1305 key; data starts * at block 1. */ unsigned char poly_key[64] = {0}; rc = mbedtls_chacha20_setkey(&c->chacha, key_bytes->data); if (rc == 0) rc = mbedtls_chacha20_starts(&c->chacha, iv_bytes->data, 0); if (rc == 0) rc = mbedtls_chacha20_update(&c->chacha, sizeof poly_key, poly_key, poly_key); if (rc == 0) rc = mbedtls_poly1305_starts(&c->poly, poly_key); mbedtls_platform_zeroize(poly_key, sizeof poly_key); break; } } if (rc != 0) { sc_release(c); sc_throw(SCR_ERR_ERROR, NULL, "Failed to initialize cipher"); goto done; } ScrDynHandleTag tag = decrypt ? SCR_DYNH_DECIPHER : SCR_DYNH_CIPHER; scr_dyn_handle_install(tag, decrypt ? &sc_ops_decipher : &sc_ops_cipher); result = scr_dyn_new_handle(c, tag); /* retains */ sc_release(c); done: scr_dyn_release(cts_mode); scr_dyn_release(xts_standard); scr_bytes_release(iv_bytes); scr_bytes_release(key_bytes); return result; } ScrDyn *scr_crypto_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn *iv, ScrDyn *options) { ScrDyn *args[3] = {cipher, key, iv}; ScrDyn *unboxed[3] = {NULL, NULL, NULL}; ScrDyn *result = NULL; for (size_t i = 0; i < 3; i++) if (!(unboxed[i] = sc_unbox(args[i]))) goto done; result = sc_cipher_new(decrypt, unboxed[0], unboxed[1], unboxed[2], options); done: for (size_t i = 0; i < 3; i++) scr_dyn_release(unboxed[i]); return result; } /* ── the cipher transforms ────────────────────────────────────────────── */ /* Raw CBC or ECB over whole blocks. */ static bool sc_cbc_blocks(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { if (c->mode == SC_ECB) { for (size_t off = 0; off < len; off += c->bs) { int rc = c->des ? mbedtls_des3_crypt_ecb(&c->des3, in + off, out + off) : mbedtls_aes_crypt_ecb(&c->aes, c->decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, in + off, out + off); if (rc != 0) return false; } return true; } if (c->des) return mbedtls_des3_crypt_cbc(&c->des3, c->decrypt ? MBEDTLS_DES_DECRYPT : MBEDTLS_DES_ENCRYPT, len, c->block, in, out) == 0; return mbedtls_aes_crypt_cbc(&c->aes, c->decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, len, c->block, in, out) == 0; } /* One forward block of the feedback modes' cipher. */ static bool sc_forward(ScCipher *c, const unsigned char *in, unsigned char *out) { return (c->des ? mbedtls_des3_crypt_ecb(&c->des3, in, out) : mbedtls_aes_crypt_ecb(&c->aes, MBEDTLS_AES_ENCRYPT, in, out)) == 0; } /* CFB (full-block feedback), CFB8, CFB1 and OFB, as OpenSSL's * CRYPTO_cfb128_encrypt, CRYPTO_cfb128_8_encrypt, CRYPTO_cfb128_1_encrypt * and CRYPTO_ofb128_encrypt: CFB and OFB keep their keystream offset across * updates; CFB1 feeds back one bit at a time, most significant first. */ static bool sc_feedback(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { unsigned char stream[16]; size_t bs = c->bs; for (size_t i = 0; i < len; i++) { switch (c->mode) { case SC_CFB: case SC_OFB: if (c->bufsz == 0 && !sc_forward(c, c->block, c->block)) return false; out[i] = (unsigned char)(in[i] ^ c->block[c->bufsz]); if (c->mode == SC_CFB) c->block[c->bufsz] = c->decrypt ? in[i] : out[i]; c->bufsz = (c->bufsz + 1) % bs; break; case SC_CFB8: { if (!sc_forward(c, c->block, stream)) return false; unsigned char byte = (unsigned char)(in[i] ^ stream[0]); memmove(c->block, c->block + 1, bs - 1); c->block[bs - 1] = c->decrypt ? in[i] : byte; out[i] = byte; break; } default: { unsigned char result = 0; for (int bit = 7; bit >= 0; bit--) { if (!sc_forward(c, c->block, stream)) return false; unsigned in_bit = (in[i] >> bit) & 1u; unsigned out_bit = in_bit ^ (stream[0] >> 7); unsigned feed = c->decrypt ? in_bit : out_bit; for (size_t k = 0; k + 1 < bs; k++) c->block[k] = (unsigned char)(c->block[k] << 1 | c->block[k + 1] >> 7); c->block[bs - 1] = (unsigned char)(c->block[bs - 1] << 1 | feed); result |= (unsigned char)(out_bit << bit); } out[i] = result; break; } } } mbedtls_platform_zeroize(stream, sizeof stream); return true; } /* chacha20 over len bytes: when the 64-bit counter's low word wraps, the * high word steps (and wraps itself, as OpenSSL's does) and the stream * restarts from block 0 of the next word. */ static bool sc_chacha20_update(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { while (len > 0) { size_t n = len < c->chacha_left ? len : (size_t)c->chacha_left; if (mbedtls_chacha20_update(&c->chacha, n, in, out) != 0) return false; in += n; out += n; len -= n; c->chacha_left -= n; if (c->chacha_left == 0) { for (int k = 4; k < 8 && ++c->block[k] == 0; k++) { } memset(c->block, 0, 4); c->chacha_left = (uint64_t)1 << 38; if (mbedtls_chacha20_starts(&c->chacha, c->block + 4, 0) != 0) return false; } } return true; } /* OpenSSL's ossl_cipher_generic_block_update for CBC and ECB: complete * blocks are processed, the remainder is buffered, and a decryption that * ends on a block boundary holds the last block back for padding removal. * Returns the output length, or -1 when the backend fails; out has room * for len + 16 bytes. */ static long sc_cbc_update(ScCipher *c, const unsigned char *in, size_t inl, unsigned char *out) { size_t bs = c->bs; size_t outl = 0; size_t nextblocks; if (c->bufsz != 0) { size_t remain = bs - c->bufsz; if (inl < remain) remain = inl; memcpy(c->buf + c->bufsz, in, remain); in += remain; inl -= remain; c->bufsz += remain; } nextblocks = inl - inl % bs; if (c->bufsz == bs && (!c->decrypt || inl > 0 || !c->pad)) { if (!sc_cbc_blocks(c, c->buf, bs, out)) return -1; c->bufsz = 0; outl = bs; out += bs; } if (nextblocks > 0) { if (c->decrypt && c->pad && nextblocks == inl) nextblocks -= bs; if (nextblocks > 0) { if (!sc_cbc_blocks(c, in, nextblocks, out)) return -1; outl += nextblocks; in += nextblocks; inl -= nextblocks; } } if (inl != 0) { memcpy(c->buf + c->bufsz, in, inl); c->bufsz += inl; } return (long)outl; } /* ossl_cipher_generic_block_final for CBC and ECB. Returns the output * length, -1 after throwing OpenSSL's error, or -2 when the backend * fails. */ static long sc_cbc_final(ScCipher *c, unsigned char out[16]) { size_t bs = c->bs; if (!c->decrypt) { if (c->pad) { unsigned char fill = (unsigned char)(bs - c->bufsz); for (size_t i = c->bufsz; i < bs; i++) c->buf[i] = fill; } else if (c->bufsz == 0) { return 0; } else if (c->bufsz != bs) { sc_throw_openssl("1C80006B", "wrong final block length", "ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH"); return -1; } if (!sc_cbc_blocks(c, c->buf, bs, out)) return -2; c->bufsz = 0; return (long)bs; } if (c->bufsz != bs) { if (c->bufsz == 0 && !c->pad) return 0; sc_throw_openssl("1C80006B", "wrong final block length", "ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH"); return -1; } if (!sc_cbc_blocks(c, c->buf, bs, c->buf)) return -2; size_t len = bs; if (c->pad) { unsigned char fill = c->buf[bs - 1]; bool bad = fill == 0 || fill > bs; for (size_t i = 0; !bad && i < fill; i++) bad = c->buf[--len] != fill; if (bad) { sc_throw_openssl("1C800064", "bad decrypt", "ERR_OSSL_BAD_DECRYPT"); return -1; } } memcpy(out, c->buf, len); c->bufsz = 0; return (long)len; } /* Constant-time comparison of the first len bytes. */ static bool sc_tag_equal(const unsigned char *a, const unsigned char *b, size_t len) { unsigned char diff = 0; for (size_t i = 0; i < len; i++) diff |= (unsigned char)(a[i] ^ b[i]); return diff == 0; } /* Poly1305 zero padding to the next 16-byte boundary of `length`. */ static bool sc_poly_pad(ScCipher *c, uint64_t length) { static const unsigned char zero[16] = {0}; size_t rem = (size_t)(length % 16); return rem == 0 || mbedtls_poly1305_update(&c->poly, zero, 16 - rem) == 0; } /* OpenSSL's chacha20-poly1305 data step: pending AAD is padded first, and * the ciphertext feeds Poly1305 unpadded until final(). */ static bool sc_chacha_update(ScCipher *c, const unsigned char *in, size_t len, unsigned char *out) { if (c->aad_open) { if (!sc_poly_pad(c, c->aad_len)) return false; c->aad_open = false; } if (c->decrypt && mbedtls_poly1305_update(&c->poly, in, len) != 0) return false; if (mbedtls_chacha20_update(&c->chacha, len, in, out) != 0) return false; if (!c->decrypt && mbedtls_poly1305_update(&c->poly, out, len) != 0) return false; c->text_len += len; return true; } static bool sc_chacha_finish(ScCipher *c, unsigned char tag[16]) { if (c->aad_open && !sc_poly_pad(c, c->aad_len)) return false; if (!sc_poly_pad(c, c->text_len)) return false; unsigned char lengths[16]; for (size_t i = 0; i < 8; i++) { lengths[i] = (unsigned char)(c->aad_len >> (8 * i)); lengths[8 + i] = (unsigned char)(c->text_len >> (8 * i)); } return mbedtls_poly1305_update(&c->poly, lengths, sizeof lengths) == 0 && mbedtls_poly1305_finish(&c->poly, tag) == 0; } /* The cipher output of an update over len bytes (+1 Buffer), or NULL * after a backend failure (for example AES-GCM's 2^36 - 32 byte message * limit) has retired the context and thrown Node's update error. */ static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len); static ScrBytes *sc_wrap_update(ScCipher *c, const unsigned char *in, size_t len); static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) { /* CCM and the key wraps, the last modes, take their own update. */ if (c->mode >= SC_CCM) return c->mode == SC_CCM ? sc_ccm_update(c, in, len) : sc_wrap_update(c, in, len); size_t room = len + 16; ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)room); out->is_buffer = true; size_t produced = 0; bool ok = true; switch (c->mode) { case SC_CBC: case SC_ECB: { long n = sc_cbc_update(c, in, len, out->data); ok = n >= 0; produced = ok ? (size_t)n : 0; break; } case SC_CTR: ok = len == 0 || mbedtls_aes_crypt_ctr(&c->aes, len, &c->bufsz, c->block, c->buf, in, out->data) == 0; produced = len; break; case SC_GCM: ok = len == 0 || mbedtls_gcm_update(&c->gcm, in, len, out->data, room, &produced) == 0; break; case SC_CHACHA: ok = len == 0 || sc_chacha_update(c, in, len, out->data); produced = len; break; case SC_CFB: case SC_CFB8: case SC_CFB1: case SC_OFB: ok = sc_feedback(c, in, len, out->data); produced = len; break; case SC_CHACHA20: ok = len == 0 || sc_chacha20_update(c, in, len, out->data); produced = len; break; case SC_CCM: case SC_WRAP: case SC_WRAP_PAD: break; } if (!ok) { mbedtls_platform_zeroize(out->data, room); scr_bytes_release(out); sc_fail(c); sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); return NULL; } out->len = produced; if (len) c->data_started = true; return out; } /* ── CCM ────────────────────────────────────────────────────────────── * OpenSSL's ccm128 (crypto/modes/ccm128.c) over the AES block, step for * step, with its provider's flags (ciphercommon_ccm.c) and Node's rules * (one update, the message-length cap, a decipher's failure reported by * final()), so every call sequence answers as Node's does. */ static void sc_ccm_block(ScCipher *c, const unsigned char in[16], unsigned char out[16]) { mbedtls_aes_crypt_ecb(&c->aes, MBEDTLS_AES_ENCRYPT, in, out); } /* CRYPTO_ccm128_setiv: the nonce and the message length. */ static void sc_ccm_setiv(ScCipher *c, uint64_t mlen) { unsigned L = c->ccm_nonce[0] & 7; if (L >= 3) { c->ccm_nonce[8] = (unsigned char)(mlen >> 56); c->ccm_nonce[9] = (unsigned char)(mlen >> 48); c->ccm_nonce[10] = (unsigned char)(mlen >> 40); c->ccm_nonce[11] = (unsigned char)(mlen >> 32); } else { memset(c->ccm_nonce + 8, 0, 8); } c->ccm_nonce[12] = (unsigned char)(mlen >> 24); c->ccm_nonce[13] = (unsigned char)(mlen >> 16); c->ccm_nonce[14] = (unsigned char)(mlen >> 8); c->ccm_nonce[15] = (unsigned char)mlen; c->ccm_nonce[0] &= (unsigned char)~0x40; memcpy(c->ccm_nonce + 1, c->block, 14 - L); } /* CRYPTO_ccm128_aad. */ static void sc_ccm_aad(ScCipher *c, const unsigned char *aad, size_t alen) { if (alen == 0) return; c->ccm_nonce[0] |= 0x40; sc_ccm_block(c, c->ccm_nonce, c->ccm_cmac); size_t i; if (alen < 0x10000 - 0x100) { c->ccm_cmac[0] ^= (unsigned char)(alen >> 8); c->ccm_cmac[1] ^= (unsigned char)alen; i = 2; } else if ((uint64_t)alen >= (uint64_t)1 << 32) { c->ccm_cmac[0] ^= 0xFF; c->ccm_cmac[1] ^= 0xFF; for (int k = 0; k < 8; k++) c->ccm_cmac[2 + k] ^= (unsigned char)((uint64_t)alen >> (56 - 8 * k)); i = 10; } else { c->ccm_cmac[0] ^= 0xFF; c->ccm_cmac[1] ^= 0xFE; for (int k = 0; k < 4; k++) c->ccm_cmac[2 + k] ^= (unsigned char)((uint64_t)alen >> (24 - 8 * k)); i = 6; } do { for (; i < 16 && alen; ++i, ++aad, --alen) c->ccm_cmac[i] ^= *aad; sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); i = 0; } while (alen); } /* ctr64_inc: the counter block's low eight bytes, big-endian. */ static void sc_ccm_ctr_inc(unsigned char *counter) { for (int n = 15; n >= 8; n--) if (++counter[n]) return; } /* CRYPTO_ccm128_encrypt and CRYPTO_ccm128_decrypt: false (-1) for a * length that differs from the one the counter block holds. */ static bool sc_ccm_crypt(ScCipher *c, const unsigned char *in, unsigned char *out, size_t len, bool decrypt) { unsigned char flags0 = c->ccm_nonce[0]; unsigned char scratch[16]; if (!(flags0 & 0x40)) sc_ccm_block(c, c->ccm_nonce, c->ccm_cmac); unsigned L = flags0 & 7; c->ccm_nonce[0] = (unsigned char)L; uint64_t n = 0; for (unsigned i = 15 - L; i < 15; ++i) { n |= c->ccm_nonce[i]; c->ccm_nonce[i] = 0; n <<= 8; } n |= c->ccm_nonce[15]; c->ccm_nonce[15] = 1; if (n != (uint64_t)len) return false; /* Each full block runs the counter block's encryption before the * CBC-MAC's, in both directions, and XORs in 64-bit words, so the two * AES operations of a block overlap. `out` may be `in`. */ while (len >= 16) { uint64_t data[2], mac[2], pad[2]; sc_ccm_block(c, c->ccm_nonce, scratch); sc_ccm_ctr_inc(c->ccm_nonce); memcpy(data, in, 16); memcpy(mac, c->ccm_cmac, 16); memcpy(pad, scratch, 16); pad[0] ^= data[0]; pad[1] ^= data[1]; /* The MAC covers the plaintext: the input when encrypting, the output * when decrypting. */ mac[0] ^= decrypt ? pad[0] : data[0]; mac[1] ^= decrypt ? pad[1] : data[1]; memcpy(out, pad, 16); memcpy(c->ccm_cmac, mac, 16); sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); in += 16; out += 16; len -= 16; } if (len) { if (decrypt) { sc_ccm_block(c, c->ccm_nonce, scratch); for (size_t i = 0; i < len; ++i) c->ccm_cmac[i] ^= (out[i] = (unsigned char)(scratch[i] ^ in[i])); sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); } else { for (size_t i = 0; i < len; ++i) c->ccm_cmac[i] ^= in[i]; sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); sc_ccm_block(c, c->ccm_nonce, scratch); for (size_t i = 0; i < len; ++i) out[i] = (unsigned char)(scratch[i] ^ in[i]); } } for (unsigned i = 15 - L; i < 16; ++i) c->ccm_nonce[i] = 0; sc_ccm_block(c, c->ccm_nonce, scratch); for (int i = 0; i < 16; i++) c->ccm_cmac[i] ^= scratch[i]; c->ccm_nonce[0] = flags0; mbedtls_platform_zeroize(scratch, sizeof scratch); return true; } /* CRYPTO_ccm128_tag: the MAC, when the counter block still holds the * tag length M (a failed call clears it). */ static bool sc_ccm_tag(const ScCipher *c, unsigned char *tag) { unsigned M = ((c->ccm_nonce[0] >> 3) & 7) * 2 + 2; if (M != (unsigned)c->tag_len) return false; memcpy(tag, c->ccm_cmac, M); return true; } /* The provider's ccm_cipher_internal for data (`out` given): the length * is set by the first data unless setAAD set it; encryption marks the tag * computed; decryption needs the tag and checks it, and on success clears * the flags so a second call fails. */ static bool sc_ccm_data(ScCipher *c, const unsigned char *in, unsigned char *out, size_t len) { if (!c->ccm_iv_set) return false; if (!c->ccm_len_set) { sc_ccm_setiv(c, len); c->ccm_len_set = true; } if (!c->decrypt) { if (!sc_ccm_crypt(c, in, out, len, false)) return false; c->ccm_tag_set = true; return true; } if (!c->ccm_tag_set) return false; unsigned char computed[16]; bool ok = sc_ccm_crypt(c, in, out, len, true) && sc_ccm_tag(c, computed) && sc_tag_equal(computed, c->tag, (size_t)c->tag_len); mbedtls_platform_zeroize(computed, sizeof computed); if (!ok) { if (len) mbedtls_platform_zeroize(out, len); return false; } c->ccm_iv_set = c->ccm_tag_set = c->ccm_len_set = false; c->ccm_verified = true; return true; } static bool sc_ccm_length_ok(const ScCipher *c, double len) { if (len <= c->ccm_max_message) return true; sc_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_MESSAGELEN", "Invalid message length"); return false; } /* update(): a decipher takes one, and its authentication failure leaves * empty output for final() to report. A decipher authenticates even an * empty message, as OpenSSL does: its tag still covers the nonce, the AAD * and the length. A cipher's empty update is skipped, and final() then * runs the empty message. */ static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len) { if (!sc_ccm_length_ok(c, (double)len)) return NULL; if (c->decrypt && c->ccm_updated) { sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); return NULL; } ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)(len ? len : 1)); out->is_buffer = true; bool ok = (len == 0 && !c->decrypt) || sc_ccm_data(c, len ? in : out->data, out->data, len); if (c->decrypt) { c->ccm_updated = true; if (!ok) c->ccm_auth_failed = true; out->len = ok ? len : 0; return out; } if (!ok) { mbedtls_platform_zeroize(out->data, len); scr_bytes_release(out); sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); return NULL; } out->len = len; return out; } /* final(): a decipher succeeds only when its update verified the tag; a * cipher runs the provider's final (an empty message when no data was * encrypted) and reads the tag, which retires the provider's state. */ static bool sc_ccm_final(ScCipher *c) { if (c->decrypt) return c->tag_state == SC_TAG_SET_BY_USER && c->ccm_verified; unsigned char dummy = 0, sink = 0; if (c->ccm_iv_set && !c->ccm_tag_set && !sc_ccm_data(c, &dummy, &sink, 0)) return false; if (!c->ccm_tag_set || !sc_ccm_tag(c, c->tag)) return false; c->tag_state = SC_TAG_COMPUTED; c->ccm_tag_set = c->ccm_iv_set = c->ccm_len_set = false; return true; } /* setAAD(buffer, { plaintextLength }): the length goes first, as Node * passes it to OpenSSL, then the AAD. False when the provider refuses. */ static bool sc_ccm_set_aad(ScCipher *c, const ScrBytes *aad, double plaintext_length) { if (!c->ccm_iv_set) return false; sc_ccm_setiv(c, (uint64_t)plaintext_length); c->ccm_len_set = true; sc_ccm_aad(c, aad->data, aad->len); return true; } /* ── key wrap ───────────────────────────────────────────────────────── * OpenSSL's CRYPTO_128_wrap, CRYPTO_128_unwrap and their RFC 5649 * padded forms (crypto/modes/wrap128.c) over the key's AES transform, and * Node's one-update rule: a failed update() leaves the cipher usable, an * empty one counts, and final() without one fails. */ static void sc_wrap_block(ScCipher *c, const unsigned char in[16], unsigned char out[16]) { mbedtls_aes_crypt_ecb(&c->aes, c->wrap_decrypt_block ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, in, out); } static void sc_wrap_xor_t(unsigned char *a, size_t t) { a[7] ^= (unsigned char)(t & 0xff); if (t > 0xff) { a[6] ^= (unsigned char)((t >> 8) & 0xff); a[5] ^= (unsigned char)((t >> 16) & 0xff); a[4] ^= (unsigned char)((t >> 24) & 0xff); } } /* CRYPTO_128_wrap: 0 unless the input is at least 16 bytes in 8-byte * blocks. `out` may be `in`. */ static size_t sc_wrap(ScCipher *c, const unsigned char *iv, unsigned char *out, const unsigned char *in, size_t inlen) { if ((inlen & 7) || inlen < 16) return 0; unsigned char b[16]; size_t t = 1; memmove(out + 8, in, inlen); memcpy(b, iv, 8); for (int j = 0; j < 6; j++) { unsigned char *r = out + 8; for (size_t i = 0; i < inlen; i += 8, t++, r += 8) { memcpy(b + 8, r, 8); sc_wrap_block(c, b, b); sc_wrap_xor_t(b, t); memcpy(r, b + 8, 8); } } memcpy(out, b, 8); mbedtls_platform_zeroize(b, sizeof b); return inlen + 8; } /* crypto_128_unwrap_raw: the plaintext into `out` and the recovered * integrity check value into `aiv`. */ static size_t sc_unwrap_raw(ScCipher *c, unsigned char aiv[8], unsigned char *out, const unsigned char *in, size_t inlen) { inlen -= 8; if ((inlen & 7) || inlen < 16) return 0; unsigned char b[16]; size_t t = 6 * (inlen >> 3); memcpy(b, in, 8); memmove(out, in + 8, inlen); for (int j = 0; j < 6; j++) { unsigned char *r = out + inlen - 8; for (size_t i = 0; i < inlen; i += 8, t--, r -= 8) { sc_wrap_xor_t(b, t); memcpy(b + 8, r, 8); sc_wrap_block(c, b, b); memcpy(r, b + 8, 8); } } memcpy(aiv, b, 8); mbedtls_platform_zeroize(b, sizeof b); return inlen; } static size_t sc_unwrap(ScCipher *c, const unsigned char *iv, unsigned char *out, const unsigned char *in, size_t inlen) { unsigned char got[8]; size_t ret = sc_unwrap_raw(c, got, out, in, inlen); if (!ret) return 0; if (!sc_tag_equal(got, iv, 8)) { mbedtls_platform_zeroize(out, ret); return 0; } return ret; } /* CRYPTO_128_wrap_pad: the 4-byte ICV and the 32-bit length, then the * data padded with zeros to 8 bytes; one block is a single encryption. */ static size_t sc_wrap_pad(ScCipher *c, const unsigned char *icv, unsigned char *out, const unsigned char *in, size_t inlen) { size_t padded = (inlen + 7) / 8 * 8; if (inlen == 0 || inlen >= ((size_t)1 << 31)) return 0; unsigned char aiv[8]; memcpy(aiv, icv, 4); aiv[4] = (unsigned char)((inlen >> 24) & 0xFF); aiv[5] = (unsigned char)((inlen >> 16) & 0xFF); aiv[6] = (unsigned char)((inlen >> 8) & 0xFF); aiv[7] = (unsigned char)(inlen & 0xFF); if (padded == 8) { memmove(out + 8, in, inlen); memcpy(out, aiv, 8); memset(out + 8 + inlen, 0, padded - inlen); sc_wrap_block(c, out, out); return 16; } memmove(out, in, inlen); memset(out + inlen, 0, padded - inlen); return sc_wrap(c, aiv, out, out, padded); } /* CRYPTO_128_unwrap_pad: the ICV, the length within the last 8 bytes and * zero padding must all check. */ static size_t sc_unwrap_pad(ScCipher *c, const unsigned char *icv, unsigned char *out, const unsigned char *in, size_t inlen) { if ((inlen & 7) || inlen < 16) return 0; size_t n = inlen / 8 - 1, padded; unsigned char aiv[8]; if (inlen == 16) { unsigned char buff[16]; sc_wrap_block(c, in, buff); memcpy(aiv, buff, 8); memcpy(out, buff + 8, 8); padded = 8; mbedtls_platform_zeroize(buff, sizeof buff); } else { padded = inlen - 8; if (sc_unwrap_raw(c, aiv, out, in, inlen) != padded) { mbedtls_platform_zeroize(out, inlen); return 0; } } size_t plain = (size_t)aiv[4] << 24 | (size_t)aiv[5] << 16 | (size_t)aiv[6] << 8 | aiv[7]; static const unsigned char zeros[8] = {0}; if (!sc_tag_equal(aiv, icv, 4) || 8 * (n - 1) >= plain || plain > 8 * n || !sc_tag_equal(out + plain, zeros, padded - plain)) { mbedtls_platform_zeroize(out, padded); return 0; } return plain; } static ScrBytes *sc_wrap_update(ScCipher *c, const unsigned char *in, size_t len) { if (c->ccm_updated) { sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); return NULL; } ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)(len + 16)); out->is_buffer = true; size_t produced = 0; if (len > 0) { bool pad = c->mode == SC_WRAP_PAD; produced = !c->decrypt ? (pad ? sc_wrap_pad(c, c->block, out->data, in, len) : sc_wrap(c, c->block, out->data, in, len)) : (pad ? sc_unwrap_pad(c, c->block, out->data, in, len) : sc_unwrap(c, c->block, out->data, in, len)); if (produced == 0) { mbedtls_platform_zeroize(out->data, len + 16); scr_bytes_release(out); sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); return NULL; } } c->ccm_updated = true; out->len = produced; return out; } /* ── output encodings ─────────────────────────────────────────────────── */ /* Node's getDecoder: the StringDecoder for this encoding, created on first * string output; a different encoding later throws ERR_INVALID_ARG_VALUE. */ static bool sc_decoder(ScCipher *c, const ScrDyn *encoding) { const char *name = encoding->kind == SCR_DYN_STR ? sc_normalize_encoding(encoding->v.str) : NULL; if (!c->decoder) { if (!name) { sc_unknown_encoding(encoding); return false; } c->decoder = scr_str_new(name, strlen(name)); c->pending = 0; return true; } if (name && strcmp(c->decoder->data, name) == 0) return true; if (!name) { sc_unknown_encoding(encoding); return false; } char rendered[160]; const char *text = scr_dyn_inspect_lite(encoding, rendered, sizeof rendered); ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "The argument 'outputEncoding' cannot be changed from '"); scr_jb_put_str(&b, c->decoder); scr_jb_puts(&b, "'. Received "); scr_jb_puts(&b, text); ScrStr *message = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_INVALID_ARG_VALUE"); scr_str_release(message); return false; } static bool sc_wants_string(const ScrDyn *encoding) { if (!scr_dyn_truthy(encoding)) return false; return !(encoding->kind == SCR_DYN_STR && encoding->v.str->len == 6 && memcmp(encoding->v.str->data, "buffer", 6) == 0); } /* The method result: the Buffer, or its StringDecoder text. Consumes out. */ static ScrDyn *sc_output(ScCipher *c, ScrBytes *out, const ScrDyn *encoding, bool end) { if (!sc_wants_string(encoding)) { ScrDyn *result = scr_dyn_new_buffer(out); scr_bytes_release(out); return result; } if (!sc_decoder(c, encoding)) { scr_bytes_release(out); return NULL; } ScrStr *text; if (end) { ScrStr *head = scr_strdec_write(c->decoder, c->pending, out); double next = scr_strdec_next(c->decoder, c->pending, out); ScrStr *tail = scr_strdec_end(c->decoder, next); text = scr_str_concat(head, tail); scr_str_release(head); scr_str_release(tail); c->pending = 0; } else { text = scr_strdec_write(c->decoder, c->pending, out); c->pending = scr_strdec_next(c->decoder, c->pending, out); } scr_bytes_release(out); ScrDyn *result = scr_dyn_new_str(text); scr_str_release(text); return result; } /* ── methods ──────────────────────────────────────────────────────────── */ static ScrDyn *sc_arg(ScrDyn *const *args, size_t argc, size_t index) { return index < argc ? args[index] : scr_dyn_undefined(); } static ScrDyn *sc_update(ScCipher *c, ScrDyn *data, ScrDyn *input_encoding, ScrDyn *output_encoding) { ScrBytes *in = NULL; if (data->kind == SCR_DYN_STR) { /* validateEncoding: hex input must have an even length. */ const char *name = input_encoding->kind == SCR_DYN_STR ? sc_normalize_encoding(input_encoding->v.str) : NULL; if (name && strcmp(name, "hex") == 0 && (unsigned long)scr_str_utf16_len(data->v.str) % 2 != 0) { char rendered[160]; const char *text = scr_dyn_inspect_lite(input_encoding, rendered, sizeof rendered); char message[256]; snprintf(message, sizeof message, "The argument 'encoding' is invalid for data of length %lu. Received %s", (unsigned long)scr_str_utf16_len(data->v.str), text); sc_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", message); return NULL; } /* C++ ParseEncoding: unknown names read as utf8. */ ScrStr *canonical = scr_str_new(name ? name : "utf8", strlen(name ? name : "utf8")); in = scr_bytes_from_str(data->v.str, canonical); scr_str_release(canonical); } else if (sc_is_view(data)) { in = sc_bytes_of(data); } else { scr_dyn_arg_type_fail("data", "of type string or an instance of Buffer, TypedArray, or DataView", data); return NULL; } if (!in) return NULL; if (!c->active) { scr_bytes_release(in); sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); return NULL; } ScrBytes *out = sc_transform(c, in->data, in->len); scr_bytes_release(in); if (!out) return NULL; return sc_output(c, out, output_encoding, false); } static bool sc_aead(const ScCipher *c) { return c->mode == SC_GCM || c->mode == SC_CHACHA || c->mode == SC_CCM; } /* final()'s output bytes (+1 Buffer), or NULL with the error thrown. */ static ScrBytes *sc_final_bytes(ScCipher *c) { if (!c->active) { sc_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INVALID_STATE", "Invalid state"); return NULL; } c->active = false; ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, 16); out->is_buffer = true; out->len = 0; bool ok = true, backend = true; switch (c->mode) { case SC_CBC: case SC_ECB: { long produced = sc_cbc_final(c, out->data); if (produced == -1) { scr_bytes_release(out); return NULL; } backend = produced >= 0; out->len = backend ? (size_t)produced : 0; break; } case SC_CTR: case SC_CFB: case SC_CFB8: case SC_CFB1: case SC_OFB: case SC_CHACHA20: break; case SC_CCM: ok = sc_ccm_final(c); break; case SC_WRAP: case SC_WRAP_PAD: backend = c->ccm_updated; break; case SC_GCM: { unsigned char full[16]; size_t ignored; backend = mbedtls_gcm_finish(&c->gcm, NULL, 0, &ignored, full, 16) == 0; if (!backend) { /* No tag is computed or compared. */ } else if (c->decrypt) { ok = c->tag_state == SC_TAG_SET_BY_USER && sc_tag_equal(full, c->tag, (size_t)c->tag_len); } else { if (c->tag_len == SC_NO_TAG) c->tag_len = 16; memcpy(c->tag, full, (size_t)c->tag_len); c->tag_state = SC_TAG_COMPUTED; } mbedtls_platform_zeroize(full, sizeof full); break; } case SC_CHACHA: { unsigned char full[16]; backend = sc_chacha_finish(c, full); if (!backend) { /* No tag is computed or compared. */ } else if (c->decrypt) { /* Decryption without setAuthTag() fails. Node 24.22 skips the check * when authTagLength was not passed either and returns unverified * plaintext; this keeps Node 24.15's behavior. */ ok = c->tag_state == SC_TAG_SET_BY_USER && sc_tag_equal(full, c->tag, (size_t)c->tag_len); } else { memcpy(c->tag, full, (size_t)c->tag_len); c->tag_state = SC_TAG_COMPUTED; } mbedtls_platform_zeroize(full, sizeof full); break; } } if (!backend) { mbedtls_platform_zeroize(out->data, 16); scr_bytes_release(out); sc_fail(c); sc_throw(SCR_ERR_ERROR, NULL, sc_aead(c) ? "Unsupported state or unable to authenticate data" : "Unsupported state"); return NULL; } if (!ok) { scr_bytes_release(out); sc_throw(SCR_ERR_ERROR, NULL, "Unsupported state or unable to authenticate data"); return NULL; } return out; } static ScrDyn *sc_final(ScCipher *c, ScrDyn *output_encoding) { ScrBytes *out = sc_final_bytes(c); return out ? sc_output(c, out, output_encoding, true) : NULL; } static ScrDyn *sc_set_aad(ScCipher *c, ScrDyn *self, ScrDyn *buffer, ScrDyn *options) { bool failed; ScrDyn *encoding = sc_string_option(options, "encoding", &failed); if (failed) return NULL; double plaintext_length = sc_uint_option(options, "plaintextLength"); if (plaintext_length == -2) { scr_dyn_release(encoding); return NULL; } ScrBytes *aad = sc_buffer_or_view(buffer, "aadbuf", encoding); scr_dyn_release(encoding); if (!aad) return NULL; if (c->mode == SC_CCM && c->active) { bool ok = true; if (plaintext_length < 0) { sc_throw(SCR_ERR_TYPE, "ERR_MISSING_ARGS", "options.plaintextLength required for CCM mode with AAD"); ok = false; } else if (!sc_ccm_length_ok(c, plaintext_length)) { ok = false; } else if (!sc_ccm_set_aad(c, aad, plaintext_length)) { sc_invalid_state("setAAD"); ok = false; } scr_bytes_release(aad); return ok ? scr_dyn_retain(self) : NULL; } /* After data, GCM refuses any AAD and chacha20-poly1305 refuses * non-empty AAD. */ bool ok = c->active && sc_aead(c) && !(c->data_started && (c->mode == SC_GCM || aad->len > 0)); bool backend = true; if (ok && c->mode == SC_GCM) backend = mbedtls_gcm_update_ad(&c->gcm, aad->data, aad->len) == 0; if (ok && c->mode == SC_CHACHA && aad->len > 0) { backend = mbedtls_poly1305_update(&c->poly, aad->data, aad->len) == 0; c->aad_len += aad->len; c->aad_open = true; } scr_bytes_release(aad); if (!backend) { sc_fail(c); ok = false; } if (!ok) { sc_invalid_state("setAAD"); return NULL; } return scr_dyn_retain(self); } static ScrDyn *sc_set_auto_padding(ScCipher *c, ScrDyn *self, ScrDyn *const *args, size_t argc) { /* The JavaScript wrapper passes !!autoPadding, so a call without an * argument turns padding off. */ bool pad = argc > 0 && scr_dyn_truthy(args[0]); if (!c->active) { sc_invalid_state("setAutoPadding"); return NULL; } c->pad = pad; return scr_dyn_retain(self); } static ScrDyn *sc_get_auth_tag(ScCipher *c) { if (c->active || !sc_aead(c) || c->tag_state != SC_TAG_COMPUTED) { sc_invalid_state("getAuthTag"); return NULL; } ScrBytes *tag = scr_bytes_new(SCR_BYTES_U8, (double)c->tag_len); tag->is_buffer = true; memcpy(tag->data, c->tag, (size_t)c->tag_len); ScrDyn *result = scr_dyn_new_buffer(tag); scr_bytes_release(tag); return result; } /* DEP0182 is emitted through process.emitWarning, so it reports on the * next tick. Node gates it on a one-shot environment flag, so a process * reports it at most once. */ static SCR_TL bool sc_dep0182_emitted = false; static void sc_dep0182_tick(void) { static const char message[] = "Using AES-GCM authentication tags of less than 128 bits without specifying " "the authTagLength option when initializing decryption is deprecated."; ScrStr *text = scr_str_new(message, sizeof message - 1); scr_emit_warning("DeprecationWarning", "DEP0182", text); scr_str_release(text); } static ScrDyn *sc_set_auth_tag(ScCipher *c, ScrDyn *self, ScrDyn *buffer, ScrDyn *encoding) { ScrBytes *tag = sc_buffer_or_view(buffer, "buffer", encoding); if (!tag) return NULL; if (!c->active || !sc_aead(c) || c->tag_state != SC_TAG_UNKNOWN) { scr_bytes_release(tag); sc_invalid_state("setAuthTag"); return NULL; } size_t len = tag->len; bool valid = c->mode == SC_GCM ? (c->tag_len == SC_NO_TAG || (size_t)c->tag_len == len) && sc_valid_gcm_tag(len) : (size_t)c->tag_len == len; if (!valid) { scr_bytes_release(tag); sc_invalid_tag((unsigned long)len); return NULL; } if (c->mode == SC_GCM && c->tag_len == SC_NO_TAG && len != 16 && !sc_dep0182_emitted) { sc_dep0182_emitted = true; scr_next_tick_raw(sc_dep0182_tick); } c->tag_len = (int)len; memcpy(c->tag, tag->data, len); c->tag_state = SC_TAG_SET_BY_USER; if (c->mode == SC_CCM) c->ccm_tag_set = true; scr_bytes_release(tag); return scr_dyn_retain(self); } /* ── Cipheriv and Decipheriv as streams ─────────────────────────────── * The stream unit builds a cipher's Transform from these operations * (ScrDynHandleTransform): each chunk goes through the handle's update(), * and the end pushes final() or reports its error, as Node's cipher.js * _transform and _flush do. */ static void *sc_stream_get(void *h) { return ((ScCipher *)h)->stream; } static void sc_stream_set(void *h, void *stream, void (*release)(void *)) { ScCipher *c = h; c->stream = stream; c->stream_release = release; } /* The handle's update(): it throws out of the write, as in Node, once * the cipher has finished. */ static ScrBytes *sc_stream_update(void *h, ScrBytes *chunk) { ScCipher *c = h; if (!c->active) { sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); return NULL; } SCR_BYTES_SNAPSHOT(chunk); return sc_transform(c, chunk->data, chunk->len * scr_bytes_elem_size(chunk->elem)); } static ScrBytes *sc_stream_final(void *h) { return sc_final_bytes(h); } static const ScrDynHandleTransform sc_transform_ops = { sc_stream_get, sc_stream_set, sc_stream_update, sc_stream_final, sc_trace, }; static ScrDynHandleOps sc_ops_cipher; static ScrDynHandleOps sc_ops_decipher; static const ScrDynHandleOps *sc_ops_of(const ScCipher *c) { return c->decrypt ? &sc_ops_decipher : &sc_ops_cipher; } static bool sc_name_is(const char *method, const char *name) { return strcmp(method, name) == 0; } /* stream.Transform members a Cipheriv inherits. */ static bool sc_stream_member(const char *name) { static const char *const members[] = { "write", "end", "pipe", "unpipe", "on", "once", "off", "addListener", "removeListener", "removeAllListeners", "emit", "read", "push", "unshift", "pause", "resume", "destroy", "setEncoding", "cork", "uncork", "setDefaultEncoding", "wrap", "prependListener", "prependOnceListener", "listeners", "listenerCount", "eventNames", "_transform", "_flush", "isPaused", "compose", "toArray", "forEach", "map", "filter", "reduce", "some", "every", "find", "drop", "take", "flatMap", "asIndexedPairs", "iterator", }; for (size_t i = 0; i < sizeof members / sizeof members[0]; i++) if (strcmp(name, members[i]) == 0) return true; return false; } static void sc_stream_fence(void) { static const char message[] = "This Cipheriv and Decipheriv stream member has no lowering when called through an untyped value"; static const char streamless[] = "Cipheriv and Decipheriv stream members called through an untyped value have no lowering in a " "program that uses no streams"; if (scr_handle_stream_hooks) scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); else scr_throw_error_msg_code(SCR_ERR_ERROR, streamless, sizeof streamless - 1, "SC2020"); } static ScrDyn *sc_dispatch(ScCipher *c, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { if (sc_name_is(method, "update")) return sc_update(c, sc_arg(args, argc, 0), sc_arg(args, argc, 1), sc_arg(args, argc, 2)); if (sc_name_is(method, "final")) return sc_final(c, sc_arg(args, argc, 0)); if (sc_name_is(method, "setAAD")) return sc_set_aad(c, self, sc_arg(args, argc, 0), sc_arg(args, argc, 1)); if (sc_name_is(method, "setAutoPadding")) return sc_set_auto_padding(c, self, args, argc); if (!c->decrypt && sc_name_is(method, "getAuthTag")) return sc_get_auth_tag(c); if (c->decrypt && sc_name_is(method, "setAuthTag")) return sc_set_auth_tag(c, self, sc_arg(args, argc, 0), sc_arg(args, argc, 1)); if (sc_stream_member(method)) { /* The members a native stream answers to checked-dynamic calls run * on the cipher's Transform; the others have no lowering. */ ScrDyn *result = NULL; if (scr_handle_stream_hooks && scr_handle_stream_hooks->call(sc_ops_of(c), c, self, method, args, argc, &result)) return result; sc_stream_fence(); return NULL; } ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, what); scr_jb_puts(&b, " is not a function"); scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); return NULL; } /* Every method reads at most three arguments. */ static ScrDyn *sc_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { ScrDyn *unboxed[3] = {NULL, NULL, NULL}; size_t count = argc < 3 ? argc : 3; ScrDyn *result = NULL; for (size_t i = 0; i < count; i++) if (!(unboxed[i] = sc_unbox(args[i]))) goto done; result = sc_dispatch(h, self, method, unboxed, count, what); done: for (size_t i = 0; i < count; i++) scr_dyn_release(unboxed[i]); return result; } static ScrDyn *sc_get(void *h, const char *key, size_t key_len) { /* The stream properties read the cipher's Transform. */ if (scr_handle_stream_hooks) { ScrDyn *value = scr_handle_stream_hooks->get(sc_ops_of(h), h, key, key_len); if (value || scr_exc_pending()) return value; } static const char message[] = "Cipheriv and Decipheriv property reads have no lowering other than the stream properties; call " "update(), final(), setAAD(), setAutoPadding(), getAuthTag() or setAuthTag()"; static const char streamless[] = "Cipheriv and Decipheriv property reads through an untyped value have no lowering in a program " "that uses no streams"; if (scr_handle_stream_hooks) scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); else scr_throw_error_msg_code(SCR_ERR_ERROR, streamless, sizeof streamless - 1, "SC2020"); return NULL; } static bool sc_set(void *h, const char *key, size_t key_len, const ScrDyn *value) { (void)h; (void)key; (void)key_len; (void)value; static const char message[] = "Writing Cipheriv and Decipheriv properties has no lowering"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return true; } static ScrDynHandleOps sc_ops_cipher = {"Cipheriv", sc_retain, sc_release, sc_invoke, sc_get, sc_set, NULL, NULL, NULL, .transform = &sc_transform_ops}; static ScrDynHandleOps sc_ops_decipher = {"Decipheriv", sc_retain, sc_release, sc_invoke, sc_get, sc_set, NULL, NULL, NULL, .transform = &sc_transform_ops};