wire-mesh-core 1.33.0 → 1.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -84,10 +84,28 @@ async function encryptNoticeContent(contentKey, plaintext) {
84
84
  async function decryptNoticeContent(contentKey, ciphertext) {
85
85
  return aesGcmDecrypt(await importContentKey(contentKey), ciphertext);
86
86
  }
87
+ /** The literal content-type suffix marking a room-notice's content as AES-256-GCM-encrypted under a room.rekey epoch's content key (room.cddl's own key-epoch obligation comment) -- the same suffix convention MIME structured syntaxes like `application/jose+json` already use, so a reader without the key still learns the notice's true underlying kind. */
88
+ const ENCRYPTED_CONTENT_TYPE_SUFFIX = "+aes256gcm";
89
+ /** The notice's own TRUE content-type, with the encryption suffix appended -- never a separate generic sentinel, so the true type stays visible without decrypting anything. Idempotent: an already-suffixed type is returned unchanged. */
90
+ function encryptedContentType(plaintextContentType_) {
91
+ return isEncryptedContentType(plaintextContentType_) ? plaintextContentType_ : `${plaintextContentType_}${ENCRYPTED_CONTENT_TYPE_SUFFIX}`;
92
+ }
93
+ /** Strips the encryption suffix back off, recovering the plaintext content-type the sender originally had. A content-type carrying no suffix is returned unchanged. */
94
+ function plaintextContentType(encryptedContentType_) {
95
+ return isEncryptedContentType(encryptedContentType_) ? encryptedContentType_.slice(0, encryptedContentType_.length - 10) : encryptedContentType_;
96
+ }
97
+ /** Whether a content-type names encrypted content (and therefore that the notice carrying it MUST also name its key-epoch -- room.cddl obligation 7). */
98
+ function isEncryptedContentType(contentType) {
99
+ return contentType.endsWith(ENCRYPTED_CONTENT_TYPE_SUFFIX);
100
+ }
87
101
  //#endregion
102
+ exports.ENCRYPTED_CONTENT_TYPE_SUFFIX = ENCRYPTED_CONTENT_TYPE_SUFFIX;
88
103
  exports.decryptNoticeContent = decryptNoticeContent;
89
104
  exports.deriveWrappingKey = deriveWrappingKey;
90
105
  exports.encryptNoticeContent = encryptNoticeContent;
106
+ exports.encryptedContentType = encryptedContentType;
91
107
  exports.generateContentKey = generateContentKey;
108
+ exports.isEncryptedContentType = isEncryptedContentType;
109
+ exports.plaintextContentType = plaintextContentType;
92
110
  exports.unwrapContentKey = unwrapContentKey;
93
111
  exports.wrapContentKey = wrapContentKey;
@@ -21,4 +21,12 @@ export declare function unwrapContentKey(wrappingKey: CryptoKey, wrapped: Uint8A
21
21
  export declare function encryptNoticeContent(contentKey: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
22
22
  /** Decrypts a room-notice's own `content`, given the content-encryption key for the epoch its key-epoch field names. Rejects on a wrong key or tampered ciphertext, same as unwrapContentKey. */
23
23
  export declare function decryptNoticeContent(contentKey: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
24
+ /** The literal content-type suffix marking a room-notice's content as AES-256-GCM-encrypted under a room.rekey epoch's content key (room.cddl's own key-epoch obligation comment) -- the same suffix convention MIME structured syntaxes like `application/jose+json` already use, so a reader without the key still learns the notice's true underlying kind. */
25
+ export declare const ENCRYPTED_CONTENT_TYPE_SUFFIX = "+aes256gcm";
26
+ /** The notice's own TRUE content-type, with the encryption suffix appended -- never a separate generic sentinel, so the true type stays visible without decrypting anything. Idempotent: an already-suffixed type is returned unchanged. */
27
+ export declare function encryptedContentType(plaintextContentType_: string): string;
28
+ /** Strips the encryption suffix back off, recovering the plaintext content-type the sender originally had. A content-type carrying no suffix is returned unchanged. */
29
+ export declare function plaintextContentType(encryptedContentType_: string): string;
30
+ /** Whether a content-type names encrypted content (and therefore that the notice carrying it MUST also name its key-epoch -- room.cddl obligation 7). */
31
+ export declare function isEncryptedContentType(contentType: string): boolean;
24
32
  //#endregion
@@ -21,4 +21,12 @@ export declare function unwrapContentKey(wrappingKey: CryptoKey, wrapped: Uint8A
21
21
  export declare function encryptNoticeContent(contentKey: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
22
22
  /** Decrypts a room-notice's own `content`, given the content-encryption key for the epoch its key-epoch field names. Rejects on a wrong key or tampered ciphertext, same as unwrapContentKey. */
23
23
  export declare function decryptNoticeContent(contentKey: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
24
+ /** The literal content-type suffix marking a room-notice's content as AES-256-GCM-encrypted under a room.rekey epoch's content key (room.cddl's own key-epoch obligation comment) -- the same suffix convention MIME structured syntaxes like `application/jose+json` already use, so a reader without the key still learns the notice's true underlying kind. */
25
+ export declare const ENCRYPTED_CONTENT_TYPE_SUFFIX = "+aes256gcm";
26
+ /** The notice's own TRUE content-type, with the encryption suffix appended -- never a separate generic sentinel, so the true type stays visible without decrypting anything. Idempotent: an already-suffixed type is returned unchanged. */
27
+ export declare function encryptedContentType(plaintextContentType_: string): string;
28
+ /** Strips the encryption suffix back off, recovering the plaintext content-type the sender originally had. A content-type carrying no suffix is returned unchanged. */
29
+ export declare function plaintextContentType(encryptedContentType_: string): string;
30
+ /** Whether a content-type names encrypted content (and therefore that the notice carrying it MUST also name its key-epoch -- room.cddl obligation 7). */
31
+ export declare function isEncryptedContentType(contentType: string): boolean;
24
32
  //#endregion
@@ -83,5 +83,19 @@ async function encryptNoticeContent(contentKey, plaintext) {
83
83
  async function decryptNoticeContent(contentKey, ciphertext) {
84
84
  return aesGcmDecrypt(await importContentKey(contentKey), ciphertext);
85
85
  }
86
+ /** The literal content-type suffix marking a room-notice's content as AES-256-GCM-encrypted under a room.rekey epoch's content key (room.cddl's own key-epoch obligation comment) -- the same suffix convention MIME structured syntaxes like `application/jose+json` already use, so a reader without the key still learns the notice's true underlying kind. */
87
+ const ENCRYPTED_CONTENT_TYPE_SUFFIX = "+aes256gcm";
88
+ /** The notice's own TRUE content-type, with the encryption suffix appended -- never a separate generic sentinel, so the true type stays visible without decrypting anything. Idempotent: an already-suffixed type is returned unchanged. */
89
+ function encryptedContentType(plaintextContentType_) {
90
+ return isEncryptedContentType(plaintextContentType_) ? plaintextContentType_ : `${plaintextContentType_}${ENCRYPTED_CONTENT_TYPE_SUFFIX}`;
91
+ }
92
+ /** Strips the encryption suffix back off, recovering the plaintext content-type the sender originally had. A content-type carrying no suffix is returned unchanged. */
93
+ function plaintextContentType(encryptedContentType_) {
94
+ return isEncryptedContentType(encryptedContentType_) ? encryptedContentType_.slice(0, encryptedContentType_.length - 10) : encryptedContentType_;
95
+ }
96
+ /** Whether a content-type names encrypted content (and therefore that the notice carrying it MUST also name its key-epoch -- room.cddl obligation 7). */
97
+ function isEncryptedContentType(contentType) {
98
+ return contentType.endsWith(ENCRYPTED_CONTENT_TYPE_SUFFIX);
99
+ }
86
100
  //#endregion
87
- export { decryptNoticeContent, deriveWrappingKey, encryptNoticeContent, generateContentKey, unwrapContentKey, wrapContentKey };
101
+ export { ENCRYPTED_CONTENT_TYPE_SUFFIX, decryptNoticeContent, deriveWrappingKey, encryptNoticeContent, encryptedContentType, generateContentKey, isEncryptedContentType, plaintextContentType, unwrapContentKey, wrapContentKey };
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_generated_protocol = require("../generated/protocol.cjs");
3
+ const require_domain_group_key = require("./group-key.cjs");
3
4
  const require_domain_room_token_verification = require("./room-token-verification.cjs");
4
5
  let cbor2 = require("cbor2");
5
6
  //#region src/domain/room.ts
@@ -91,6 +92,11 @@ async function verifyRoomNotice(notice, options) {
91
92
  ok: false,
92
93
  reason: "content_expired"
93
94
  };
95
+ const keyEpoch = claims["key-epoch"];
96
+ if (require_domain_group_key.isEncryptedContentType(claims["content-type"]) !== (keyEpoch !== void 0)) return {
97
+ ok: false,
98
+ reason: "key_epoch_mismatch"
99
+ };
94
100
  const tokenVerdict = await require_domain_room_token_verification.verifyRoomToken(claims.token, {
95
101
  identity: options.identity,
96
102
  clock: options.clock,
@@ -130,7 +136,8 @@ async function createRoomNotice(options) {
130
136
  "content-type": options.contentType,
131
137
  content: options.content,
132
138
  ...options.refs !== void 0 ? { refs: [...options.refs] } : {},
133
- ...options.validUntil !== void 0 ? { "valid-until": options.validUntil } : {}
139
+ ...options.validUntil !== void 0 ? { "valid-until": options.validUntil } : {},
140
+ ...options.keyEpoch !== void 0 ? { "key-epoch": options.keyEpoch } : {}
134
141
  });
135
142
  const protectedHeader = protectedHeaderFor(options.identity);
136
143
  return [
@@ -4,7 +4,7 @@ import { t as Clock } from "../clock-DiSx-WKM.cjs";
4
4
  import { a as RevocationCheck } from "../tokens-C9Kwy0Gk.cjs";
5
5
  import { RoomTokenVerdictReason } from "./room-token-verification.cjs";
6
6
  //#region src/domain/room.d.ts
7
- export type RoomNoticeVerdictReason = "malformed" | "wrong_room" | "bad_signature" | "wrong_poster" | "content_expired" | RoomTokenVerdictReason;
7
+ export type RoomNoticeVerdictReason = "malformed" | "wrong_room" | "bad_signature" | "wrong_poster" | "content_expired" | "key_epoch_mismatch" | RoomTokenVerdictReason;
8
8
  export type RoomNoticeVerdict = {
9
9
  ok: true;
10
10
  claims: RoomNoticeClaims;
@@ -45,6 +45,8 @@ export interface CreateRoomNoticeOptions {
45
45
  content: Uint8Array<ArrayBuffer>;
46
46
  refs?: readonly MessageRef[];
47
47
  validUntil?: number;
48
+ /** Which room.rekey epoch content is encrypted under (wire-mesh#141), present iff content-type names an encrypted content-type -- see room-notice-claims' own obligation 7 comment in room.cddl. This function does not itself encrypt content: the caller encrypts via group-key.ts's encryptNoticeContent beforehand and passes the resulting ciphertext as content, exactly as it would pass any other opaque bytes. */
49
+ keyEpoch?: number;
48
50
  }
49
51
  /**
50
52
  * Mints one self-certifying room-notice as `identity` -- the counterpart to verifyRoomNotice above, producing exactly what it accepts. `posted-at` is always stamped from the injected clock, never caller-supplied, matching obligation 4's own requirement that a poster's own log stay non-decreasing: a caller backdating its own posted-at would be indistinguishable from a clock bug at mint time, so this function simply doesn't expose the field to override.
@@ -4,7 +4,7 @@ import { t as Clock } from "../clock-DiSx-WKM.mjs";
4
4
  import { a as RevocationCheck } from "../tokens-k172l0Iy.mjs";
5
5
  import { RoomTokenVerdictReason } from "./room-token-verification.mjs";
6
6
  //#region src/domain/room.d.ts
7
- export type RoomNoticeVerdictReason = "malformed" | "wrong_room" | "bad_signature" | "wrong_poster" | "content_expired" | RoomTokenVerdictReason;
7
+ export type RoomNoticeVerdictReason = "malformed" | "wrong_room" | "bad_signature" | "wrong_poster" | "content_expired" | "key_epoch_mismatch" | RoomTokenVerdictReason;
8
8
  export type RoomNoticeVerdict = {
9
9
  ok: true;
10
10
  claims: RoomNoticeClaims;
@@ -45,6 +45,8 @@ export interface CreateRoomNoticeOptions {
45
45
  content: Uint8Array<ArrayBuffer>;
46
46
  refs?: readonly MessageRef[];
47
47
  validUntil?: number;
48
+ /** Which room.rekey epoch content is encrypted under (wire-mesh#141), present iff content-type names an encrypted content-type -- see room-notice-claims' own obligation 7 comment in room.cddl. This function does not itself encrypt content: the caller encrypts via group-key.ts's encryptNoticeContent beforehand and passes the resulting ciphertext as content, exactly as it would pass any other opaque bytes. */
49
+ keyEpoch?: number;
48
50
  }
49
51
  /**
50
52
  * Mints one self-certifying room-notice as `identity` -- the counterpart to verifyRoomNotice above, producing exactly what it accepts. `posted-at` is always stamped from the injected clock, never caller-supplied, matching obligation 4's own requirement that a poster's own log stay non-decreasing: a caller backdating its own posted-at would be indistinguishable from a clock bug at mint time, so this function simply doesn't expose the field to override.
@@ -1,4 +1,5 @@
1
1
  import { roomNoticeClaimsSchema } from "../generated/protocol.mjs";
2
+ import { isEncryptedContentType } from "./group-key.mjs";
2
3
  import { verifyRoomToken } from "./room-token-verification.mjs";
3
4
  import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2";
4
5
  //#region src/domain/room.ts
@@ -90,6 +91,11 @@ async function verifyRoomNotice(notice, options) {
90
91
  ok: false,
91
92
  reason: "content_expired"
92
93
  };
94
+ const keyEpoch = claims["key-epoch"];
95
+ if (isEncryptedContentType(claims["content-type"]) !== (keyEpoch !== void 0)) return {
96
+ ok: false,
97
+ reason: "key_epoch_mismatch"
98
+ };
93
99
  const tokenVerdict = await verifyRoomToken(claims.token, {
94
100
  identity: options.identity,
95
101
  clock: options.clock,
@@ -129,7 +135,8 @@ async function createRoomNotice(options) {
129
135
  "content-type": options.contentType,
130
136
  content: options.content,
131
137
  ...options.refs !== void 0 ? { refs: [...options.refs] } : {},
132
- ...options.validUntil !== void 0 ? { "valid-until": options.validUntil } : {}
138
+ ...options.validUntil !== void 0 ? { "valid-until": options.validUntil } : {},
139
+ ...options.keyEpoch !== void 0 ? { "key-epoch": options.keyEpoch } : {}
133
140
  });
134
141
  const protectedHeader = protectedHeaderFor(options.identity);
135
142
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wire-mesh-core",
3
- "version": "1.33.0",
3
+ "version": "1.35.0",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c",
6
6
  "repository": {