Refactor chat system to remove E2EE and simplify messaging
Removed all E2EE chat endpoints, crypto logic, and related API routes, transitioning chat to plain text storage and transport. Updated chat send/receive endpoints to use signed actions and enforce DM privacy settings. Cleaned up Swarm chat inbox, key management, and related debug endpoints. Updated user profile to support DM privacy, improved search for handle queries, and refactored conversation/message logic to support the new model. Migrated bot settings and privacy settings to new locations.
This commit is contained in:
+3
-110
@@ -43,9 +43,6 @@ export const users = pgTable('users', {
|
||||
headerUrl: text('header_url'),
|
||||
privateKeyEncrypted: text('private_key_encrypted'), // For cryptographic signing
|
||||
publicKey: text('public_key').notNull(),
|
||||
// E2E Chat keys (ECDH) - separate from signing keys
|
||||
chatPublicKey: text('chat_public_key'), // ECDH public key for chat encryption
|
||||
chatPrivateKeyEncrypted: text('chat_private_key_encrypted'), // Encrypted with user's password
|
||||
nodeId: uuid('node_id').references(() => nodes.id),
|
||||
// Bot-related fields
|
||||
isBot: boolean('is_bot').default(false).notNull(),
|
||||
@@ -69,6 +66,7 @@ export const users = pgTable('users', {
|
||||
followingCount: integer('following_count').default(0).notNull(),
|
||||
postsCount: integer('posts_count').default(0).notNull(),
|
||||
website: text('website'),
|
||||
dmPrivacy: text('dm_privacy').default('everyone').notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
}, (table) => [
|
||||
@@ -927,12 +925,8 @@ export const chatMessages = pgTable('chat_messages', {
|
||||
senderNodeDomain: text('sender_node_domain'), // null if local
|
||||
senderDid: text('sender_did'), // DID for Signal Protocol
|
||||
|
||||
// Message content (encrypted for recipient with their public key)
|
||||
encryptedContent: text('encrypted_content').notNull(),
|
||||
// Sender's copy (encrypted with sender's public key so they can read their own messages)
|
||||
senderEncryptedContent: text('sender_encrypted_content'),
|
||||
// Sender's ECDH public key (for E2E decryption by recipient)
|
||||
senderChatPublicKey: text('sender_chat_public_key'),
|
||||
// Message content (plain text for verified chat)
|
||||
content: text('content'),
|
||||
|
||||
// Swarm sync info
|
||||
swarmMessageId: text('swarm_message_id').unique(), // Format: swarm:domain:uuid
|
||||
@@ -956,108 +950,7 @@ export const chatMessagesRelations = relations(chatMessages, ({ one }) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
/**
|
||||
* V2.1 E2EE: Device Bundles
|
||||
* Stores the identity and signed prekeys for each user device.
|
||||
* Verified by the root ECDSA identity key (signature).
|
||||
*/
|
||||
export const chatDeviceBundles = pgTable('chat_device_bundles', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
did: text('did').notNull(),
|
||||
|
||||
// The device identifier (UUID generated by client)
|
||||
deviceId: text('device_id').notNull(),
|
||||
|
||||
// Signal Protocol fields
|
||||
registrationId: integer('registration_id'),
|
||||
|
||||
// X25519 Identity Key (Base64)
|
||||
identityKey: text('identity_key').notNull(),
|
||||
|
||||
// Signed PreKey (JSON: { id, key, sig })
|
||||
signedPreKey: text('signed_pre_key').notNull(),
|
||||
|
||||
// Kyber PreKey for post-quantum security (JSON: { id, key, sig })
|
||||
kyberPreKey: text('kyber_pre_key'),
|
||||
|
||||
// One-Time Keys (JSON array of { id, key }) - cached/uploaded batch
|
||||
// Note: Individual keys are usually stored in a separate table for atomic consumption,
|
||||
// but initial upload can be here or we strictly use chat_one_time_keys.
|
||||
// We will use the separate table for consumption tracking.
|
||||
|
||||
// The ECDSA signature covering (did, deviceId, identityKey, signedPreKey)
|
||||
// Signed by the user's Root Identity Key (P-256)
|
||||
signature: text('signature').notNull(),
|
||||
|
||||
lastSeenAt: timestamp('last_seen_at').defaultNow().notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
}, (table) => [
|
||||
index('chat_bundles_user_idx').on(table.userId),
|
||||
index('chat_bundles_did_idx').on(table.did),
|
||||
// compound index for fast lookup of a specific device
|
||||
uniqueIndex('chat_bundles_device_unique').on(table.userId, table.deviceId),
|
||||
]);
|
||||
|
||||
export const chatDeviceBundlesRelations = relations(chatDeviceBundles, ({ one, many }) => ({
|
||||
user: one(users, {
|
||||
fields: [chatDeviceBundles.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
oneTimeKeys: many(chatOneTimeKeys),
|
||||
}));
|
||||
|
||||
/**
|
||||
* V2.1 E2EE: One-Time Prekeys
|
||||
* Pool of disposable keys for X3DH.
|
||||
*/
|
||||
export const chatOneTimeKeys = pgTable('chat_one_time_keys', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
bundleId: uuid('bundle_id').notNull().references(() => chatDeviceBundles.id, { onDelete: 'cascade' }),
|
||||
|
||||
keyId: integer('key_id').notNull(),
|
||||
publicKey: text('public_key').notNull(), // X25519 Base64
|
||||
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
}, (table) => [
|
||||
index('chat_otk_bundle_idx').on(table.bundleId),
|
||||
// Ensure (bundleId, keyId) is unique
|
||||
uniqueIndex('chat_otk_unique').on(table.bundleId, table.keyId),
|
||||
]);
|
||||
|
||||
export const chatOneTimeKeysRelations = relations(chatOneTimeKeys, ({ one }) => ({
|
||||
bundle: one(chatDeviceBundles, {
|
||||
fields: [chatOneTimeKeys.bundleId],
|
||||
references: [chatDeviceBundles.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
/**
|
||||
* V2.1 E2EE: Chat Inbox
|
||||
* Stores encrypted envelopes waiting for delivery to a specific device.
|
||||
*/
|
||||
export const chatInbox = pgTable('chat_inbox', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
// Recipient info
|
||||
recipientDid: text('recipient_did').notNull(),
|
||||
recipientDeviceId: text('recipient_device_id'), // Null means "all devices" or "not yet routed"
|
||||
|
||||
// Sender info
|
||||
senderDid: text('sender_did').notNull(),
|
||||
|
||||
// The Envelope (SignedAction format)
|
||||
// Contains: { action: "chat.deliver", did, handle, ts, nonce, data: { ciphertext, header... }, sig }
|
||||
envelope: text('envelope_json').notNull(),
|
||||
|
||||
isRead: boolean('is_read').default(false).notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
expiresAt: timestamp('expires_at').notNull(), // TTL (e.g. 30 days)
|
||||
}, (table) => [
|
||||
index('chat_inbox_recipient_idx').on(table.recipientDid, table.recipientDeviceId),
|
||||
index('chat_inbox_created_idx').on(table.createdAt),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Typing indicators for real-time chat UX.
|
||||
|
||||
Reference in New Issue
Block a user