Files
scriptc/packages/runtime/src/scr_cipher.c
T

1928 lines
74 KiB
C

/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 <value>". */
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 <value>". */
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};