EdgeAIG's picture
download
raw
12.5 kB
/**
* Signs and verifies event-log session authentication payloads.
*
* Remote peers use this challenge-response flow to prove that they control a
* session signing key before sending session traffic. The signed payload
* includes the remote id, a short-lived challenge, the event-log public key, and
* the signing public key in a stable byte format.
*
* @since 4.0.0
*/
import * as Data from "../../Data.js";
import * as Effect from "../../Effect.js";
const textEncoder = /*#__PURE__*/new TextEncoder();
const textDecoder = /*#__PURE__*/new TextDecoder("utf-8", {
fatal: true
});
const constLengthPrefixBytes = 4;
/**
* Defines the domain-separation string embedded in canonical session
* authentication payloads.
*
* **When to use**
*
* Use when you need the domain-separation string used to build canonical
* event-log session authentication payloads.
*
* @category constants
* @since 4.0.0
*/
export const AuthPayloadContext = "eventlog-auth-v1";
/**
* Defines the required byte length for raw Ed25519 public keys used in session
* authentication.
*
* **When to use**
*
* Use when implementing session-auth serialization or validation that must
* reject public keys with a non-canonical raw byte length.
*
* @category constants
* @since 4.0.0
*/
export const Ed25519PublicKeyLength = 32;
/**
* Defines the required byte length for Ed25519 signatures used in session authentication.
*
* **When to use**
*
* Use when implementing session-auth verification that must reject signatures
* with a non-canonical byte length before cryptographic checking.
*
* @category constants
* @since 4.0.0
*/
export const Ed25519SignatureLength = 64;
/**
* Defines the number of random bytes generated for a session authentication
* challenge.
*
* **When to use**
*
* Use when you need the challenge size for event-log session authentication.
*
* @category constants
* @since 4.0.0
*/
export const SessionAuthChallengeLength = 32;
/**
* Defines the time-to-live, in milliseconds, for a pending session
* authentication challenge.
*
* **When to use**
*
* Use when you need the timeout for pending event-log session authentication
* challenges.
*
* @category constants
* @since 4.0.0
*/
export const SessionAuthChallengeTimeToLiveMillis = 30_000;
/**
* Error raised while encoding, decoding, signing, verifying, or generating
* session authentication challenges.
*
* @category errors
* @since 4.0.0
*/
export class EventLogSessionAuthError extends /*#__PURE__*/Data.TaggedError("EventLogSessionAuthError") {}
const toArrayBuffer = data => {
const copy = new Uint8Array(data.byteLength);
copy.set(data);
return copy.buffer;
};
const decodeUtf8 = bytes => Effect.try({
try: () => textDecoder.decode(bytes),
catch: cause => new EventLogSessionAuthError({
reason: "InvalidPayload",
message: "Session auth payload contains invalid UTF-8 bytes",
cause
})
});
const assertSigningPublicKeyLength = signingPublicKey => {
if (signingPublicKey.byteLength === Ed25519PublicKeyLength) return Effect.void;
return Effect.fail(new EventLogSessionAuthError({
reason: "InvalidSigningPublicKeyLength",
message: `Expected signingPublicKey length to be ${Ed25519PublicKeyLength} bytes, received ${signingPublicKey.byteLength}`
}));
};
const assertSignatureLength = signature => {
if (signature.byteLength === Ed25519SignatureLength) return Effect.void;
return Effect.fail(new EventLogSessionAuthError({
reason: "InvalidSignatureLength",
message: `Expected signature length to be ${Ed25519SignatureLength} bytes, received ${signature.byteLength}`
}));
};
const getSubtle = /*#__PURE__*/Effect.suspend(() => {
const subtle = globalThis.crypto?.subtle;
if (subtle === undefined) {
return Effect.fail(new EventLogSessionAuthError({
reason: "CryptoUnavailable",
message: "globalThis.crypto.subtle is not available"
}));
}
return Effect.succeed(subtle);
});
const getCrypto = /*#__PURE__*/Effect.suspend(() => {
const crypto = globalThis.crypto;
if (crypto === undefined) {
return Effect.fail(new EventLogSessionAuthError({
reason: "CryptoUnavailable",
message: "globalThis.crypto is not available"
}));
}
return Effect.succeed(crypto);
});
const writeLength = (target, offset, length) => {
if (length < 0 || length > 0xffff_ffff) {
return Effect.fail(new EventLogSessionAuthError({
reason: "InvalidPayload",
message: `Invalid canonical field length: ${length}`
}));
}
target[offset] = length >>> 24 & 0xff;
target[offset + 1] = length >>> 16 & 0xff;
target[offset + 2] = length >>> 8 & 0xff;
target[offset + 3] = length & 0xff;
return Effect.succeed(offset + constLengthPrefixBytes);
};
const readLength = (source, offset) => (source[offset] << 24 | source[offset + 1] << 16 | source[offset + 2] << 8 | source[offset + 3]) >>> 0;
const readField = (payload, state) => {
if (state.offset + constLengthPrefixBytes > payload.byteLength) {
return Effect.fail(new EventLogSessionAuthError({
reason: "InvalidPayload",
message: "Session auth payload is truncated before field length"
}));
}
const length = readLength(payload, state.offset);
state.offset += constLengthPrefixBytes;
if (state.offset + length > payload.byteLength) {
return Effect.fail(new EventLogSessionAuthError({
reason: "InvalidPayload",
message: "Session auth payload is truncated inside a field"
}));
}
const field = payload.slice(state.offset, state.offset + length);
state.offset += length;
return Effect.succeed(field);
};
const bytesToHex = bytes => {
let hex = "";
for (const byte of bytes) {
hex += byte.toString(16).padStart(2, "0");
}
return hex;
};
const encodeRemoteIdField = remoteId => typeof remoteId === "string" ? textEncoder.encode(remoteId) : textEncoder.encode(bytesToHex(remoteId));
/**
* Encodes a session authentication payload into the canonical byte format.
*
* **Details**
*
* The canonical payload format uses ordered big-endian length-prefixed fields:
*
* 1. context (fixed: eventlog-auth-v1)
* 2. remoteId
* 3. challenge bytes
* 4. publicKey
* 5. signingPublicKey bytes
*
* @category encoding
* @since 4.0.0
*/
export const encodeSessionAuthPayload = /*#__PURE__*/Effect.fnUntraced(function* (payload) {
yield* assertSigningPublicKeyLength(payload.signingPublicKey);
const fields = [textEncoder.encode(AuthPayloadContext), encodeRemoteIdField(payload.remoteId), payload.challenge, textEncoder.encode(payload.publicKey), payload.signingPublicKey];
const totalLength = fields.reduce((total, field) => total + constLengthPrefixBytes + field.byteLength, 0);
const encoded = new Uint8Array(totalLength);
let offset = 0;
for (const field of fields) {
offset = yield* writeLength(encoded, offset, field.byteLength);
encoded.set(field, offset);
offset += field.byteLength;
}
return encoded;
});
/**
* Decodes a canonical session authentication payload.
*
* **Details**
*
* The decoder validates the context field, UTF-8 fields, signing public key
* length, and rejects truncated or trailing bytes.
*
* @category encoding
* @since 4.0.0
*/
export const decodeSessionAuthPayload = /*#__PURE__*/Effect.fnUntraced(function* (payload) {
const state = {
offset: 0
};
const context = yield* decodeUtf8(yield* readField(payload, state));
if (context !== AuthPayloadContext) {
return yield* new EventLogSessionAuthError({
reason: "InvalidContext",
message: `Invalid session auth payload context: ${context}`
});
}
const remoteId = yield* decodeUtf8(yield* readField(payload, state));
const challenge = yield* readField(payload, state);
const publicKey = yield* decodeUtf8(yield* readField(payload, state));
const signingPublicKey = yield* readField(payload, state);
yield* assertSigningPublicKeyLength(signingPublicKey);
if (state.offset !== payload.byteLength) {
return yield* new EventLogSessionAuthError({
reason: "InvalidPayload",
message: "Session auth payload contains trailing bytes"
});
}
return {
remoteId,
challenge,
publicKey,
signingPublicKey
};
});
/**
* Creates a canonical session authentication signature with an Ed25519 private key.
*
* **Details**
*
* The private key must be PKCS#8-encoded bytes importable by `SubtleCrypto`.
*
* @category signing
* @since 4.0.0
*/
export const signSessionAuthPayloadBytes = /*#__PURE__*/Effect.fnUntraced(function* (options) {
yield* decodeSessionAuthPayload(options.payload);
const subtle = yield* getSubtle;
let privateKey = yield* Effect.tryPromise({
try: () => subtle.importKey("pkcs8", toArrayBuffer(options.signingPrivateKey), "Ed25519", false, ["sign"]),
catch: cause => new EventLogSessionAuthError({
reason: "InvalidSigningPrivateKey",
message: "Failed to import Ed25519 signing private key (expected PKCS#8 bytes)",
cause
})
});
const signature = yield* Effect.tryPromise({
try: () => subtle.sign("Ed25519", privateKey, toArrayBuffer(options.payload)),
catch: cause => new EventLogSessionAuthError({
reason: "CryptoFailure",
message: "Failed to sign canonical session auth payload",
cause
})
});
return new Uint8Array(signature);
});
/**
* Verifies an Ed25519 signature for canonical session authentication payload
* bytes.
*
* **Details**
*
* The payload, signing public key, and signature lengths are validated before
* calling `SubtleCrypto.verify`.
*
* @category verification
* @since 4.0.0
*/
export const verifySessionAuthPayloadBytes = /*#__PURE__*/Effect.fnUntraced(function* (options) {
yield* decodeSessionAuthPayload(options.payload);
yield* assertSigningPublicKeyLength(options.signingPublicKey);
yield* assertSignatureLength(options.signature);
const subtle = yield* getSubtle;
const publicKey = yield* Effect.tryPromise({
try: () => subtle.importKey("raw", toArrayBuffer(options.signingPublicKey), "Ed25519", false, ["verify"]),
catch: cause => new EventLogSessionAuthError({
reason: "InvalidSigningPublicKeyLength",
message: "Failed to import Ed25519 signing public key",
cause
})
});
return yield* Effect.tryPromise({
try: () => subtle.verify("Ed25519", publicKey, toArrayBuffer(options.signature), toArrayBuffer(options.payload)),
catch: cause => new EventLogSessionAuthError({
reason: "CryptoFailure",
message: "Failed to verify canonical session auth payload signature",
cause
})
});
});
/**
* Encodes a session authentication payload in canonical form and signs it with an
* Ed25519 private key.
*
* @category signing
* @since 4.0.0
*/
export const signSessionAuthPayload = options => encodeSessionAuthPayload(options).pipe(Effect.flatMap(payload => signSessionAuthPayloadBytes({
payload,
signingPrivateKey: options.signingPrivateKey
})));
/**
* Encodes a session authentication payload in canonical form and verifies its
* Ed25519 signature.
*
* @category verification
* @since 4.0.0
*/
export const verifySessionAuthPayload = options => encodeSessionAuthPayload(options).pipe(Effect.flatMap(payload => verifySessionAuthPayloadBytes({
payload,
signingPublicKey: options.signingPublicKey,
signature: options.signature
})));
/**
* Generates a random session authentication challenge using `globalThis.crypto`.
*
* @category challenge
* @since 4.0.0
*/
export const makeSessionAuthChallenge = /*#__PURE__*/Effect.gen(function* () {
const crypto = yield* getCrypto;
const challenge = new Uint8Array(SessionAuthChallengeLength);
crypto.getRandomValues(challenge);
return challenge;
});
/**
* Verifies an authentication request by requiring the `Ed25519` algorithm and
* checking the signature over the canonical session authentication payload.
*
* @category verification
* @since 4.0.0
*/
export const verifySessionAuthenticateRequest = /*#__PURE__*/Effect.fnUntraced(function* (options) {
if (options.algorithm !== "Ed25519") {
return yield* new EventLogSessionAuthError({
reason: "InvalidAlgorithm",
message: `Unsupported session auth algorithm: ${options.algorithm}`
});
}
return yield* verifySessionAuthPayload({
remoteId: options.remoteId,
challenge: options.challenge,
publicKey: options.publicKey,
signingPublicKey: options.signingPublicKey,
signature: options.signature
});
});
//# sourceMappingURL=EventLogSessionAuth.js.map

Xet Storage Details

Size:
12.5 kB
·
Xet hash:
d2ed91cc14817d0961e4ceaba99acad9cab3368b4d14109f773389ecf6a4f1b9

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.