[{"data":1,"prerenderedAt":611},["ShallowReactive",2],{"search-de":3,"content-de-developer\u002Fsealed-mail-at-rest":4,"surround-de-\u002Fdeveloper\u002Fsealed-mail-at-rest":602},[],{"id":5,"title":6,"body":7,"description":594,"extension":595,"meta":596,"navigation":597,"path":598,"seo":599,"stem":600,"__hash__":601},"content_de\u002F3.developer\u002F21.sealed-mail-at-rest.md","Sealed Mail: Nachrichtentexte im Ruhezustand",{"type":8,"value":9,"toc":583},"minimark",[10,32,64,69,72,193,198,208,218,229,246,257,287,291,298,376,380,425,429,455,483,494,501,548,557,561,574],[11,12,13,14,18,19,23,24,27,28,31],"p",{},"Owlat versiegelt ",[15,16,17],"strong",{},"jeden gespeicherten Nachrichtentext"," — sowohl die Inline-Body-Spalten\nals auch die Storage-Blobs (die rohen ",[20,21,22],"code",{},".eml","- und die Blobs großer Nachrichtentexte) — mit\neinem einzigen instanzeigenen Datenschlüssel, sodass ein Datenbank- ",[15,25,26],{},"oder Storage","-Dump\nChiffrat statt lesbarer Mail enthält. Das ist die „At-Rest“-Schicht von Sealed Mail (E8b) —\nzu unterscheiden von der PGP\u002FMIME-Verschlüsselung zwischen Instanzen, die Mail\n",[15,29,30],{},"auf der Leitung"," schützt.",[33,34,36],"callout",{"type":35},"info",[11,37,38,41,42,45,46,49,50,55,56,59,60,63],{},[15,39,40],{},"Abdeckung."," Jede Body-Oberfläche wird sowohl ",[15,43,44],{},"ab jetzt"," versiegelt (jeder\nproduktive Schreibpfad versiegelt beim Speichern) als auch ",[15,47,48],{},"rückwirkend"," (die\nfortsetzbare Back-Fill-Migration versiegelt bestehende Zeilen und Blobs). In-Process-Leser\nentschlüsseln über die Accessor-Ebene; die nackten Signed-URL-Blob-Konsumenten\n(Web-Reader, IMAP-Bridge, ausgehender MTA, Rohdownload) holen sich die Daten über den\n",[51,52,54],"a",{"href":53},"#storage-blobs","Entschlüsselungs-Proxy"," ",[20,57,58],{},"\u002Fsealed-blob",". Das Versiegeln setzt voraus, dass\n",[20,61,62],{},"INSTANCE_SECRET"," konfiguriert ist; auf einem echten Deployment ist es das stets, sodass ein\nDatenbank- oder Storage-Dump Chiffrat und keine lesbare Mail enthält.",[65,66,68],"h2",{"id":67},"was-versiegelt-wird","Was versiegelt wird",[11,70,71],{},"Jede Body-tragende Oberfläche über alle Nachrichtenformen hinweg wird versiegelt:",[73,74,75,88],"table",{},[76,77,78],"thead",{},[79,80,81,85],"tr",{},[82,83,84],"th",{},"Oberfläche",[82,86,87],{},"Versiegelt",[89,90,91,109,126,139,158,171],"tbody",{},[79,92,93,99],{},[94,95,96],"td",{},[20,97,98],{},"inboundMessages",[94,100,101,104,105,108],{},[20,102,103],{},"textBody",", ",[20,106,107],{},"htmlBody"," (Inline-Bodys der AI-Inbox)",[79,110,111,117],{},[94,112,113,116],{},[20,114,115],{},"mailMessages"," (inline)",[94,118,119,104,122,125],{},[20,120,121],{},"textBodyInline",[20,123,124],{},"htmlBodyInline"," (Snippet des persönlichen Postfachs)",[79,127,128,133],{},[94,129,130],{},[20,131,132],{},"unifiedMessages",[94,134,135,138],{},[20,136,137],{},"content"," (der JSON-Body-Blob)",[79,140,141,146],{},[94,142,143],{},[20,144,145],{},"mailDrafts",[94,147,148,104,151,104,154,157],{},[20,149,150],{},"bodyHtml",[20,152,153],{},"bodyText",[20,155,156],{},"bodyBlocks"," (Entwürfe im Editor)",[79,159,160,165],{},[94,161,162],{},[20,163,164],{},"conversationThreads",[94,166,167,170],{},[20,168,169],{},"lastPreview"," (Zeilenvorschau der Team-Inbox)",[79,172,173,178],{},[94,174,175,177],{},[20,176,115],{}," (Blobs)",[94,179,180,183,184,186,187,104,190],{},[20,181,182],{},"rawStorageId"," (rohes ",[20,185,22],{},"), ",[20,188,189],{},"textBodyStorageId",[20,191,192],{},"htmlBodyStorageId",[194,195,197],"h3",{"id":196},"die-chiffre","Die Chiffre",[11,199,200,201,203,204,207],{},"Der Versiegelungsschlüssel wird aus ",[20,202,62],{}," per HKDF-SHA256 mit einem\nversionsgepinnten Salt und Info-Label (",[20,205,206],{},"owlat:at-rest:bodies:v1",") abgeleitet, was ihn\ngegenüber jeder anderen Verwendung des Instanz-Secrets domänensepariert (Zugangsdaten\nexterner Mail, MTA-Transport-Secrets, der E2EE-Schlüsseltresor). Bodys werden mit\nAES-256-GCM verschlüsselt und als selbstbeschreibender Envelope-String gespeichert:",[209,210,215],"pre",{"className":211,"code":213,"language":214},[212],"language-text","atrest:1:\u003Cbase64(iv)>:\u003Cbase64(ciphertext‖gcmTag)>\n","text",[20,216,213],{"__ignoreMap":217},"",[11,219,220,221,224,225,228],{},"Das Präfix ",[20,222,223],{},"atrest:"," und die Version erlauben es einem Leser, einen versiegelten Wert\n",[15,226,227],{},"ohne den Schlüssel"," von einem Alt-Klartextwert zu unterscheiden — genau das macht die\nBack-Fill-Migration fortsetzbar: Eine halb migrierte Tabelle ist eine Mischung aus\nversiegelten und Klartextzeilen, und jeder Leser toleriert beides.",[11,230,231,232,235,236,238,239,241,242,245],{},"Die Erkennung des Versiegelungszustands ist ",[15,233,234],{},"strukturell streng"," und kein bloßer\nPräfixtest: Nachrichtentexte sind angreiferkontrolliert und können buchstäblich mit\n",[20,237,223],{}," beginnen, deshalb gilt ein Wert nur dann als versiegelt, wenn er ein\nwohlgeformter Envelope ist (exakt vier Doppelpunkt-Teile, eine bekannte numerische\nVersion, kanonisches Base64, ein 12-Byte-IV und ein Chiffrat von mindestens der Länge des\n16-Byte-GCM-Tags). Ein Klartext-Body, der lediglich mit ",[20,240,223],{}," beginnt, wird wörtlich\ngelesen und nie entschlüsselt. In der Gegenrichtung ist die Idempotenzprüfung des\nVersiegelungspfads ",[15,243,244],{},"schlüsselgebunden"," — ein Wert gilt nur dann als bereits versiegelt,\nwenn er sich tatsächlich unter dem Schlüssel dieser Instanz entschlüsseln lässt —, sodass\nein envelope-förmiger Klartext wie jeder andere Body verschlüsselt und nicht für Chiffrat\ngehalten wird.",[11,247,248,249,252,253,256],{},"Die gesamte Chiffre liegt in ",[20,250,251],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FatRestBodies.ts",", und der einzige\nEntschlüsselungs-Chokepoint ist ",[20,254,255],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FmessageBody.ts",": Jeder Body-Leser\nläuft durch diese Accessoren, sodass „beim Lesen entsiegeln“ eine einzige Heimat hat,\nstatt über die Codebasis verstreut zu sein.",[11,258,259,260,263,264,266,267,270,271,274,275,278,279,282,283,286],{},"Storage-",[15,261,262],{},"Blobs"," können Nicht-UTF-8-Bytes tragen (8-Bit-MIME, binäre Anhänge im rohen\n",[20,265,22],{},"), daher nutzen sie ein ",[15,268,269],{},"byteweises"," Geschwister derselben Konstruktion —\nAES-256-GCM unter einem domänenseparierten Schlüssel (",[20,272,273],{},"owlat:at-rest:blobs:v1",") mit einem\nkompakten Binär-Envelope (",[20,276,277],{},"ARBLB1","-Magic + Version + IV + Chiffrat) — in derselben\n",[20,280,281],{},"lib\u002FatRestBodies.ts",". Die Blob-Helfer und der Auslieferungspfad liegen in\n",[20,284,285],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FsealedBlob.ts",".",[65,288,290],{"id":289},"bewusste-ausnahmen-bleiben-klartext","Bewusste Ausnahmen (bleiben Klartext)",[11,292,293,294,297],{},"Drei Oberflächen bleiben ",[15,295,296],{},"absichtlich"," klartextabgeleitet. Jede ist an ihrer\nSchema-Index-Definition annotiert, damit die Ausnahme im Code auffindbar ist:",[299,300,301,351,364],"ul",{},[302,303,304,307,308,311,312],"li",{},[15,305,306],{},"Volltext-Suchfelder."," Convex indexiert den Klartext eines ",[20,309,310],{},"searchField",", eine\nVersiegelung würde die serverseitige Suche also zerstören. Diese Felder halten ein\nkurzes Snippet oder extrahierte Schlüsselwörter, nie den vollständigen Body:\n",[299,313,314,324,332,340,346],{},[302,315,316,319,320,323],{},[20,317,318],{},"mailMessages.snippet"," (",[20,321,322],{},"search_messages",")",[302,325,326,319,329,323],{},[20,327,328],{},"knowledgeEntries.searchableText",[20,330,331],{},"search_knowledge",[302,333,334,319,337,323],{},[20,335,336],{},"semanticFiles.searchableText",[20,338,339],{},"search_files",[302,341,342,343],{},"Suchfelder von ",[20,344,345],{},"contacts",[302,347,342,348],{},[20,349,350],{},"campaigns",[302,352,353,356,357,104,360,363],{},[15,354,355],{},"Vektor-Embeddings."," Beim Ingest aus dem Klartext abgeleitet und als Floats\ngespeichert (",[20,358,359],{},"vector_knowledge",[20,361,362],{},"vector_files","). Sie sind nicht auf den Quelltext\numkehrbar und werden für die semantische Suche benötigt.",[302,365,366,55,369,55,372,375],{},[15,367,368],{},"Datenexport pro Kontakt.",[20,370,371],{},"contacts\u002FdataExport.ts",[15,373,374],{},"entschlüsselt"," Bodys beim\nErstellen eines DSGVO-Auskunftspakets — das eigene Datenpaket des Betroffenen muss\nlesbar sein. Das ist die eine dokumentierte Stelle, an der Klartext den Speicher verlässt.",[65,377,379],{"id":378},"back-fill-migration","Back-Fill-Migration",[11,381,382,385,386,389,390,393,394,397,398,401,402,404,405,408,409,412,413,416,417,420,421,424],{},[20,383,384],{},"apps\u002Fapi\u002Fconvex\u002Fmigrations\u002F0035_seal_bodies_at_rest.ts"," durchläuft jede Body-tragende\nOberfläche seitenweise — cursortragende interne Mutationen für die Inline-Spalten und\neine interne ",[15,387,388],{},"Action"," für die Storage-Blobs (Blob-Inhalte sind nur aus einer Action\nlesbar) —, angetrieben von einem fortsetzbaren ",[20,391,392],{},"run","-Orchestrator bis zum Abschluss. Sie ist\nidempotent (ein erneuter Lauf überspringt bereits versiegelte Zeilen und Blobs) und lässt\nmitten im Lauf nie eine Zeile unlesbar zurück. Weil Convex-Storage pro ID unveränderlich\nist, liest das Versiegeln eines Blobs ihn ein, speichert die versiegelte Kopie unter einer\nneuen ID, biegt die Zeile um und löscht den alten Klartext-Blob erst, nachdem die Zeile auf\ndie versiegelte Kopie zeigt. Das Versiegeln ist ",[15,395,396],{},"sharing-bewusst",": IMAP ",[20,399,400],{},"COPY"," teilt einen\nStorage-Blob über Zeilen hinweg (die Kopie-Zeile verwendet ",[20,403,182],{},"\u002F",[20,406,407],{},"*BodyStorageId","\ndes Originals wieder), deshalb biegt das erneute Versiegeln ",[15,410,411],{},"jede"," Zeile um, die auf\neinen alten Blob verweist — gefunden über die Indizes ",[20,414,415],{},"by_raw_storage"," \u002F\n",[20,418,419],{},"by_text_body_storage"," \u002F ",[20,422,423],{},"by_html_body_storage"," —, bevor er in einer einzigen Mutation\ngelöscht wird. Eine Geschwisterkopie bleibt somit nie auf einen gelöschten Blob zeigen.",[65,426,428],{"id":427},"storage-blobs","Storage-Blobs",[11,430,431,432,434,435,437,438,440,441,444,445,448,449,451,452,286],{},"Das rohe ",[20,433,22],{}," unter ",[20,436,182],{}," und die ",[20,439,407],{},"-Blobs großer Nachrichtentexte\nsind im Ruhezustand mit der Byte-Chiffre versiegelt. Sie werden weiterhin an die\nNackt-URL-Konsumenten ausgeliefert — den Postbox-Web-Reader (",[20,442,443],{},"fetch(url).text()","), die\nprozessexterne IMAP-Bridge (",[20,446,447],{},"FETCH RFC822","), den ausgehenden MTA \u002F den externen\nSMTP-Worker (der das ",[20,450,22],{}," zum Übertragen holt) und den Rohdownload —, doch statt einer\nblanken signierten Storage-URL erhalten diese Aufrufer nun eine\n",[15,453,454],{},"Entschlüsselungs-Proxy-URL",[11,456,457,319,460,463,464,467,468,471,472,474,475,478,479,482],{},[20,458,459],{},"GET \u002Fsealed-blob",[20,461,462],{},"apps\u002Fapi\u002Fconvex\u002Fmail\u002FsealedBlobHttp.ts",") liest den versiegelten Blob,\nden ein ",[15,465,466],{},"Capability-Token"," benennt, entsiegelt ihn und streamt die Klartext-Bytes. Das\nToken ist ein HMAC über ",[20,469,470],{},"storageId . contentType . expiry",", geschlüsselt mit\n",[20,473,62],{},", und wird erst ausgestellt, nachdem der Aufrufer an der Query-Stelle\nautorisiert wurde (die Postfach-Eigentümerschaft wird geprüft, bevor irgendeine URL\nzurückgegeben wird) — passend zur unerratbaren, zeitlich begrenzten Natur der signierten\nConvex-URL, die es ersetzt. Ein ungültiges\u002Fabgelaufenes\u002Fgefälschtes Token ergibt schlicht\n",[20,476,477],{},"403",". Weil jeder Konsument weiterhin ein einfaches ",[20,480,481],{},"GET"," macht, das die Originalbytes\nliefert, ist der Proxy ein transparenter Drop-in und kein prozessexterner Code muss geändert\nwerden.",[11,484,485,486,489,490,493],{},"Der In-Process-Lesepfad (",[20,487,488],{},"readMailMessageText",") entsiegelt Blobs direkt über\n",[20,491,492],{},"readSealedBlobText",", sodass serverseitige Body-Lesevorgänge nie Chiffrat berühren.",[11,495,496,497,500],{},"Eine Mutation kann die Bytes eines Blobs weder lesen noch neu speichern (Blob-Inhalte sind\nnur aus Actions zugänglich), deshalb versiegeln die zwei Pfade, die einen\n",[15,498,499],{},"vom Worker hochgeladenen Klartext-Blob"," annehmen, ihn außerhalb der Reihe, statt ein\nstehendes Klartext-Residuum zu hinterlassen:",[299,502,503,522],{},[302,504,505,319,508,511,512,514,515,104,518,521],{},[15,506,507],{},"IMAP APPEND",[20,509,510],{},"mail.imap.appendMessage",") lädt das rohe ",[20,513,22],{}," direkt in den Storage\nund plant nach dem Einfügen der Zeile eine Reseal-Action pro Nachricht ein\n(",[20,516,517],{},"resealMessageBlobs",[20,519,520],{},"runAfter(0)",").",[302,523,524,319,527,530,531,534,535,537,538,540,541,420,544,547],{},[15,525,526],{},"Externer IMAP-Sync",[20,528,529],{},"mail.externalDelivery.ingestExternalMessage",") versiegelt bereits\nbeim Schreiben: Sein einziger Aufrufer ",[20,532,533],{},"ingestExternalRaw"," ist eine ",[15,536,388],{},", die das\nrohe ",[20,539,22],{}," und die Body-Blobs versiegelt (",[20,542,543],{},"storeSealedBlob",[20,545,546],{},"splitBodyForStorage","),\nbevor die Mutation läuft.",[11,549,550,552,553,556],{},[20,551,517],{}," ist idempotent (ein bereits versiegelter Blob wird beim erneuten\nVersiegeln zum No-Op) und biegt in einer Mutation um und löscht den alten Klartext-Blob. Im\nkurzen Fenster zwischen dem Klartext-Schreibvorgang und dem eingeplanten erneuten Versiegeln\nliest und liefert der Blob über die mischtoleranten Accessoren und den Proxy korrekt aus\n(",[20,554,555],{},"openBytesAtRest"," reicht Alt-Klartext durch). Der Back-Fill bleibt der Nachholpfad für\nZeilen, die vor dem Ausliefern von E8b geschrieben wurden.",[65,558,560],{"id":559},"abnahme","Abnahme",[11,562,563,564,567,568,571,572],{},"Ein Datenbank- ",[15,565,566],{},"und Storage","-Dump einer eingespielten, migrierten Instanz enthält\n",[15,569,570],{},"null Klartext von Nachrichtentexten"," — über die fünf Inline-Body-Formen und das rohe ",[20,573,22],{},[299,575,576],{},[302,577,578,579,582],{},"Body-Blobs hinweg — abgesehen von der oben dokumentierten Suchindex-Ausnahme. Belegt wird\ndas durch den Canary-Test in\n",[20,580,581],{},"apps\u002Fapi\u002Fconvex\u002F__tests__\u002FsealBodiesAtRest.integration.test.ts",", der sowohl die\nDB-Spalten als auch die gespeicherten Blob-Bytes dumpt.",{"title":217,"searchDepth":584,"depth":584,"links":585},2,[586,590,591,592,593],{"id":67,"depth":584,"text":68,"children":587},[588],{"id":196,"depth":589,"text":197},3,{"id":289,"depth":584,"text":290},{"id":378,"depth":584,"text":379},{"id":427,"depth":584,"text":428},{"id":559,"depth":584,"text":560},"Wie Owlat jeden gespeicherten Nachrichtentext mit einem Instanz-Datenschlüssel versiegelt und welche Ausnahmen im Suchindex bewusst im Klartext bleiben.","md",{},true,"\u002Fdeveloper\u002Fsealed-mail-at-rest",{"title":6,"description":594},"3.developer\u002F21.sealed-mail-at-rest","mjauYAij4C224LE34YjJyFibEMy55b7nsK6NM5zgJTU",[603,607],{"title":604,"path":605,"stem":606,"children":-1},"Plattformbetrieb","\u002Fdeveloper\u002Fplatform-operations","3.developer\u002F20.platform-operations",{"title":608,"path":609,"stem":610,"children":-1},"Transportsicherheit","\u002Fdeveloper\u002Ftransport-security","3.developer\u002F22.transport-security",1786915108435]