Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 40 additions & 6 deletions src/commons/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,7 @@ export async function tkt48(seed, ctx) {
// salt for hkdf can be zero if secret is pseudorandom
// but a fixed salt is needed for high-entropy
// but non uniform keys like outputs of DHKE
export async function gen(secret, info, salt = fixedsalt) {
if (emptyBuf(secret) || emptyBuf(info)) {
throw new Error("empty secret/info");
}

async function gen(secret, info, salt = fixedsalt) {
const key = await hkdfhmac(secret, info, salt);
return crypto.subtle.exportKey("raw", key);
}
Expand All @@ -52,7 +48,7 @@ export async function gen(secret, info, salt = fixedsalt) {
// cendyne.dev/posts/2023-01-30-how-to-use-hkdf.html
// info adds entropy to extracted keys, and must be unique:
// see: soatok.blog/2021/11/17/understanding-hkdf
async function hkdfhmac(skmac, usectx, salt = new Uint8Array(0)) {
export async function hkdfhmac(skmac, usectx, salt = new Uint8Array(0)) {
const dk = await hkdf(skmac);
return await crypto.subtle.deriveKey(
hkdf256(salt, usectx),
Expand Down Expand Up @@ -108,6 +104,15 @@ export function aesgcm256opts() {
};
}

/**
*
* @param {CryptoKey} aeskey
* @param {BufferSource} iv
* @param {BufferSource} plaintext
* @param {BufferSource?} aad
* @returns {Promise<Uint8Array>}
* @throws {Error} if encryption fails
*/
export async function encryptAesGcm(aeskey, iv, plaintext, aad) {
if (!aad || emptyBuf(aad)) {
aad = undefined; // ZEROBUF is not the same as null?
Expand All @@ -127,6 +132,14 @@ export async function encryptAesGcm(aeskey, iv, plaintext, aad) {
return normalize8(taggedciphertext);
}

/**
* @param {CryptoKey} aeskey
* @param {BufferSource} iv
* @param {BufferSource} taggedciphertext
* @param {BufferSource?} aad
* @returns {Promise<Uint8Array>} - The decrypted plaintext
* @throws {Error} - If decryption fails
*/
export async function decryptAesGcm(aeskey, iv, taggedciphertext, aad) {
if (!aad || emptyBuf(aad)) {
aad = undefined; // ZEROBUF is not the same as null?
Expand All @@ -145,3 +158,24 @@ export async function decryptAesGcm(aeskey, iv, taggedciphertext, aad) {
);
return normalize8(plaintext);
}

/**
* @param {CryptoKey} ck - The HMAC key
* @param {BufferSource} m - message to sign
* @returns {Promise<ArrayBuffer>} - The HMAC signature
* @throws {Error} - If the key is not valid or signing fails
*/
export function hmacsign(ck, m) {
return crypto.subtle.sign("HMAC", ck, m);
}

/**
* @param {CryptoKey} ck - The HMAC key
* @param {ArrayBuffer} mac - The HMAC signature to verify
* @param {BufferSource} m - The message to verify against
* @returns {Promise<boolean>} - True if the signature is valid, false otherwise
* @throws {Error} - If the key is not valid or verification fails
*/
export function hmacverify(ck, mac, m) {
return crypto.subtle.verify("HMAC", ck, mac, m);
}
77 changes: 53 additions & 24 deletions src/core/node/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
decryptAesGcm,
hkdfaes,
hkdfalgkeysz,
hkdfhmac,
hmacsign,
sha512,
} from "../../commons/crypto.js";
import * as envutil from "../../commons/envutil.js";
import * as util from "../../commons/util.js";

const ctx = bufutil.fromStr("encryptcrossservice");
const encctx = bufutil.fromStr("encryptcrossservice");
const macctx = bufutil.fromStr("authorizecrossservice");

/**
* @param {String} TLS_CRT_KEY - Contains base64 (no wrap) encoded key and
Expand Down Expand Up @@ -46,37 +48,59 @@ export function getCertKeyFromEnv(TLS_CRT_KEY) {

/**
* @param {X509Certificate} replacing - The X509Certificate to replace the existing one
* @returns {Promise<[BufferSource, BufferSource]>} - The key and certificate as ArrayBuffers
* @returns {Promise<[BufferSource|null, BufferSource|null]>} - The key and certificate as ArrayBuffers
*/
export async function replaceKeyCert(replacing) {
if (replacing == null) return [null, null];
const nokeycert = [null, null];

if (replacing == null) return nokeycert;
if (
replacing.subject.indexOf("rethinkdns.com") < 0 ||
replacing.subjectAltName.indexOf("rethinkdns.com") < 0
) {
return [null, null];
return nokeycert;
}

try {
const req = new Request("https://redir.nile.workers.dev/x/crt", {
const [aeskey, mackey] = await keys();
if (!aeskey || !mackey) {
log.e("certfile: key missing");
return nokeycert;
}

const now = Date.now();
const u = "https://redir.nile.workers.dev/x/crt/" + now;
const url = new URL(u);
const msg = bufutil.fromStr(url.pathname);
const authz = await hmacsign(mackey, msg);
const req = new Request(url, {
method: "GET",
headers: {
"x-rethinkdns-xsvc-authz": bufutil.hex(authz),
},
});
const r = await fetch(req);

if (!r.ok) {
log.e("certfile: fetch failed", authz.length, r.status, r.statusText);
return nokeycert;
}

const crthex = await r.text();
if (util.emptyString(crthex)) {
log.e("certfile: empty response");
return [null, null];
return nokeycert;
}

const crtkey = await decryptText(req, crthex);
if (util.emptyString(crtkey)) {
log.e("certfile: empty enc(crtkey)");
return [null, null];
return nokeycert;
}
const [key, cert] = getCertKeyFromEnv(crtkey);
if (bufutil.emptyBuf(key) || bufutil.emptyBuf(cert)) {
log.e("certfile: key/cert empty");
return [null, null];
return nokeycert;
}

const latest = new X509Certificate(cert);
Expand All @@ -85,12 +109,12 @@ export async function replaceKeyCert(replacing) {
latest.subjectAltName.indexOf("rethinkdns.com") < 0
) {
log.e("certfile: latest cert subject mismatch", latest.subject);
return [null, null];
return nokeycert;
}

if (latest.serialNumber === replacing.serialNumber) {
log.d("certfile: latest cert same as replacing", latest.serialNumber);
return [null, null];
return [key, cert];
}

const latestUntil = new Date(latest.validTo);
Expand All @@ -106,7 +130,7 @@ export async function replaceKeyCert(replacing) {
"now",
Date.now()
);
return [null, null];
return nokeycert;
}

log.i("certfile: latest cert", latest.serialNumber, "until", latestUntil);
Expand All @@ -115,7 +139,7 @@ export async function replaceKeyCert(replacing) {
} catch (err) {
log.e("certfile: failed to get cert", err);
}
return [null, null];
return nokeycert;
}

/**
Expand All @@ -135,6 +159,7 @@ export async function decryptText(req, ivciphertaghex) {
try {
const iv = ivciphertag.slice(0, 12); // first 12 bytes are iv
const ciphertag = ivciphertag.slice(12); // rest is cipher text + tag
// crypto.junod.info/posts/recursive-hash/#data-serialization
// 1 Aug 2025 => "5/7/2025" => Friday, 7th month (0-indexed), 2025
const aadstr =
now.getUTCDay() +
Expand Down Expand Up @@ -162,8 +187,8 @@ export async function decryptText(req, ivciphertaghex) {
aad.length
);

const aeskey = await key();
if (!aeskey) {
const [aeskey, mackey] = await keys();
if (!aeskey || !mackey) {
log.e("decrypt: key missing");
return null;
}
Expand All @@ -181,39 +206,43 @@ export async function decryptText(req, ivciphertaghex) {
}

/**
* @returns {Promise<CryptoKey|null>} - Returns a CryptoKey or null if the key is missing or invalid
* @returns {Promise<[CryptoKey|null]>} - Returns a CryptoKey or null if the key is missing or invalid
*/
async function key() {
if (bufutil.emptyBuf(ctx)) {
async function keys() {
const nokeys = [null, null];
if (bufutil.emptyBuf(encctx) || bufutil.emptyBuf(macctx)) {
log.e("key: ctx missing");
return null;
return nokeys;
}

const skhex = envutil.kdfSvcSecretHex();
if (util.emptyString(skhex)) {
log.e("key: KDF_SVC missing");
return null;
return nokeys;
}

const sk = bufutil.hex2buf(skhex);
if (bufutil.emptyBuf(sk)) {
log.e("key: kdf seed conv empty");
return null;
return nokeys;
}

if (sk.length < hkdfalgkeysz) {
log.e("keygen: seed too short", sk.length, hkdfalgkeysz);
return null;
return nokeys;
}

try {
const sk256 = sk.slice(0, hkdfalgkeysz);
// info must always of a fixed size for ALL KDF calls
const info512 = await sha512(ctx);
const info512enc = await sha512(encctx);
const info512mac = await sha512(macctx);
// exportable: crypto.subtle.exportKey("raw", key);
// log.d("key fingerprint", bufutil.hex(await sha512(bufutil.concat(sk, info512)));

return hkdfaes(sk256, info512);
const aeskey = await hkdfaes(sk256, info512enc);
const mackey = await hkdfhmac(sk256, info512mac);
return [aeskey, mackey];
} catch (ignore) {
log.d("keygen: err", ignore);
}
Expand Down
Loading