@vex-chat/libvex 6.6.3 → 6.7.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.
@@ -54,6 +54,8 @@ import { type Kysely, sql } from "kysely";
54
54
  import { effectiveMessageRetentionHintDays } from "../retention.js";
55
55
  import { parseSkippedKeysStrict } from "../utils/ratchet.js";
56
56
 
57
+ const STORAGE_MESSAGE_BLOB_PREFIX = "vex-storage-message:1\n";
58
+
57
59
  export class SqliteStorage extends EventEmitter implements Storage {
58
60
  public ready = false;
59
61
  /** 32-byte AES-256 (or nacl) key for local at-rest `secretbox` (see `XUtils.deriveLocalAtRestAesKey`). */
@@ -323,6 +325,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
323
325
  .addColumn("sender", "text")
324
326
  .addColumn("recipient", "text")
325
327
  .addColumn("group", "text")
328
+ .addColumn("extra", "text")
326
329
  .addColumn("mailID", "text")
327
330
  .addColumn("message", "text")
328
331
  .addColumn("direction", "text")
@@ -370,6 +373,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
370
373
  )
371
374
  .execute();
372
375
  await this.ensureSessionRatchetColumns();
376
+ await this.ensureMessageExtraColumn();
373
377
  await this.ensureRetentionHintColumn();
374
378
  await this.ensureMessageMailIdIndex();
375
379
 
@@ -534,16 +538,17 @@ export class SqliteStorage extends EventEmitter implements Storage {
534
538
  return;
535
539
  }
536
540
 
537
- // Encrypt plaintext with at-rest key before saving to disk
541
+ // Encrypt plaintext with at-rest key before saving to disk.
542
+ const storedPlaintext = encodeStoredMessagePlaintext(message);
538
543
  const fips = getCryptoProfile() === "fips";
539
544
  const ct = fips
540
545
  ? await xSecretboxAsync(
541
- XUtils.decodeUTF8(message.message),
546
+ XUtils.decodeUTF8(storedPlaintext),
542
547
  XUtils.decodeHex(message.nonce),
543
548
  this.atRestAesKey,
544
549
  )
545
550
  : xSecretbox(
546
- XUtils.decodeUTF8(message.message),
551
+ XUtils.decodeUTF8(storedPlaintext),
547
552
  XUtils.decodeHex(message.nonce),
548
553
  this.atRestAesKey,
549
554
  );
@@ -559,6 +564,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
559
564
  authorID: message.authorID,
560
565
  decrypted: message.decrypted ? 1 : 0,
561
566
  direction: message.direction,
567
+ extra: null,
562
568
  forward: message.forward ? 1 : 0,
563
569
  group: message.group ?? null,
564
570
  mailID: message.mailID,
@@ -733,6 +739,10 @@ export class SqliteStorage extends EventEmitter implements Storage {
733
739
  }
734
740
  const direction =
735
741
  msg.direction === "incoming" ? "incoming" : "outgoing";
742
+ const storedPlaintext = decodeStoredMessagePlaintext(
743
+ plaintext,
744
+ msg.extra,
745
+ );
736
746
  const rowMessage: Message = {
737
747
  authorID: msg.authorID,
738
748
  decrypted: decryptedFlag,
@@ -740,7 +750,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
740
750
  forward: msg.forward !== 0,
741
751
  group: msg.group,
742
752
  mailID: msg.mailID,
743
- message: plaintext,
753
+ ...storedPlaintext,
744
754
  nonce: msg.nonce,
745
755
  readerID: msg.readerID,
746
756
  recipient: msg.recipient,
@@ -773,6 +783,16 @@ export class SqliteStorage extends EventEmitter implements Storage {
773
783
  };
774
784
  }
775
785
 
786
+ private async ensureMessageExtraColumn(): Promise<void> {
787
+ try {
788
+ await sql
789
+ .raw("ALTER TABLE messages ADD COLUMN extra text")
790
+ .execute(this.db);
791
+ } catch {
792
+ // Existing databases may already have this column.
793
+ }
794
+ }
795
+
776
796
  /** Speeds up mailID existence checks for saveMessage deduplication. */
777
797
  private async ensureMessageMailIdIndex(): Promise<void> {
778
798
  try {
@@ -965,3 +985,61 @@ export class SqliteStorage extends EventEmitter implements Storage {
965
985
  });
966
986
  }
967
987
  }
988
+
989
+ function decodeStoredMessagePlaintext(
990
+ plaintext: string,
991
+ rowExtra: null | string,
992
+ ): Pick<Message, "extra" | "message"> {
993
+ if (!plaintext.startsWith(STORAGE_MESSAGE_BLOB_PREFIX)) {
994
+ return {
995
+ ...(rowExtra !== null ? { extra: rowExtra } : {}),
996
+ message: plaintext,
997
+ };
998
+ }
999
+
1000
+ try {
1001
+ const raw = JSON.parse(
1002
+ plaintext.slice(STORAGE_MESSAGE_BLOB_PREFIX.length),
1003
+ ) as unknown;
1004
+ if (!isJsonRecord(raw)) {
1005
+ return {
1006
+ ...(rowExtra !== null ? { extra: rowExtra } : {}),
1007
+ message: plaintext,
1008
+ };
1009
+ }
1010
+ const message = raw["message"];
1011
+ if (typeof message !== "string") {
1012
+ return {
1013
+ ...(rowExtra !== null ? { extra: rowExtra } : {}),
1014
+ message: plaintext,
1015
+ };
1016
+ }
1017
+ const extra = raw["extra"];
1018
+ return {
1019
+ ...(extra === null || typeof extra === "string" ? { extra } : {}),
1020
+ message,
1021
+ };
1022
+ } catch {
1023
+ return {
1024
+ ...(rowExtra !== null ? { extra: rowExtra } : {}),
1025
+ message: plaintext,
1026
+ };
1027
+ }
1028
+ }
1029
+
1030
+ function encodeStoredMessagePlaintext(message: Message): string {
1031
+ if (message.extra === undefined) {
1032
+ return message.message;
1033
+ }
1034
+ return (
1035
+ STORAGE_MESSAGE_BLOB_PREFIX +
1036
+ JSON.stringify({
1037
+ extra: message.extra,
1038
+ message: message.message,
1039
+ })
1040
+ );
1041
+ }
1042
+
1043
+ function isJsonRecord(value: unknown): value is Record<string, unknown> {
1044
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1045
+ }