feat(broker/db): e2e file encryption schema + db functions
- add mesh.file_key table (fileId, peerPubkey, sealedKey, grantedByPubkey) - add encrypted + ownerPubkey columns to mesh.file - export insertFileKeys, getFileKey, grantFileKey from broker.ts - update uploadFile/getFile/listFiles to include encrypted/ownerPubkey - migration 0012_add-file-encryption applied to prod Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
13
packages/db/migrations/0012_add-file-encryption.sql
Normal file
13
packages/db/migrations/0012_add-file-encryption.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
ALTER TABLE "mesh"."file" ADD COLUMN "encrypted" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "mesh"."file" ADD COLUMN "owner_pubkey" text;--> statement-breakpoint
|
||||
CREATE TABLE "mesh"."file_key" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"file_id" text NOT NULL,
|
||||
"peer_pubkey" text NOT NULL,
|
||||
"sealed_key" text NOT NULL,
|
||||
"granted_at" timestamp DEFAULT now() NOT NULL,
|
||||
"granted_by_pubkey" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "mesh"."file_key" ADD CONSTRAINT "file_key_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "mesh"."file"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "file_key_file_peer_idx" ON "mesh"."file_key" ("file_id","peer_pubkey");
|
||||
@@ -305,6 +305,8 @@ export const meshFile = meshSchema.table("file", {
|
||||
minioKey: text().notNull(),
|
||||
tags: text().array().default([]),
|
||||
persistent: boolean().notNull().default(true),
|
||||
encrypted: boolean().notNull().default(false),
|
||||
ownerPubkey: text(),
|
||||
uploadedByName: text(),
|
||||
uploadedByMember: text().references(() => meshMember.id),
|
||||
targetSpec: text(), // null = entire mesh
|
||||
@@ -327,6 +329,29 @@ export const meshFileAccess = meshSchema.table("file_access", {
|
||||
accessedAt: timestamp().defaultNow().notNull(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Per-peer encrypted symmetric keys for E2E encrypted files.
|
||||
* The file body is encrypted with a random key (Kf); Kf is sealed
|
||||
* (crypto_box_seal) to each authorized peer's X25519 pubkey and stored here.
|
||||
*/
|
||||
export const meshFileKey = meshSchema.table("file_key", {
|
||||
id: text().primaryKey().notNull().$defaultFn(generateId),
|
||||
fileId: text()
|
||||
.references(() => meshFile.id, { onDelete: "cascade" })
|
||||
.notNull(),
|
||||
peerPubkey: text().notNull(),
|
||||
sealedKey: text().notNull(),
|
||||
grantedAt: timestamp().defaultNow().notNull(),
|
||||
grantedByPubkey: text(),
|
||||
});
|
||||
|
||||
export const meshFileKeyRelations = relations(meshFileKey, ({ one }) => ({
|
||||
file: one(meshFile, {
|
||||
fields: [meshFileKey.fileId],
|
||||
references: [meshFile.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Per-peer context snapshot. Each peer (presence) has at most one context
|
||||
* entry per mesh, upserted on each share_context call. Allows peers to
|
||||
@@ -531,6 +556,10 @@ export type SelectMeshFile = typeof meshFile.$inferSelect;
|
||||
export type InsertMeshFile = typeof meshFile.$inferInsert;
|
||||
export type SelectMeshFileAccess = typeof meshFileAccess.$inferSelect;
|
||||
export type InsertMeshFileAccess = typeof meshFileAccess.$inferInsert;
|
||||
export const selectMeshFileKeySchema = createSelectSchema(meshFileKey);
|
||||
export const insertMeshFileKeySchema = createInsertSchema(meshFileKey);
|
||||
export type SelectMeshFileKey = typeof meshFileKey.$inferSelect;
|
||||
export type InsertMeshFileKey = typeof meshFileKey.$inferInsert;
|
||||
export const selectMeshContextSchema = createSelectSchema(meshContext);
|
||||
export const insertMeshContextSchema = createInsertSchema(meshContext);
|
||||
export const selectMeshTaskSchema = createSelectSchema(meshTask);
|
||||
|
||||
Reference in New Issue
Block a user