Repository navigation
fix: no mail data in the errors of an encrypted mail job - #14
Conversation
With Mail.queue(message, encrypt: true), the job data is encrypted. But a send error can name the recipient, eg. an SMTP reply like "550 <ann@example.com>: mailbox unavailable". The worker writes the error to its log and to the failed jobs in plain text, so the address leaked. Now the encrypted mail job catches the send error and throws a RedactedMailError. Every email address and every text of the mail is replaced by ***. The error type, the SMTP or HTTP code and the stack trace stay, to help find the problem. Mail jobs without encrypt keep the full error, as before.
Bot reviewThe idea is right. One change in Must-fix
Nits
Checked and OK
|
redact() made one RegExp per text of the mail. For a text over about 30 KB, Dart throws "Regular expression too large", and the text of that error holds the start of the body. It came out of handle(), so the worker logged it and saved it. The real send error was lost. Now redact() uses a plain search, and skips a text that is longer than the error. When the redaction fails, the error has only the type and the code. A text must be a whole word, and tags are skipped, so a tag like smtp does not hide SMTP: in the error. The email regex has length limits, so it stays fast on a long error. The README and the changelog now say what is really hidden.
Fixer replyMust-fix:
Nits:
Tests (new
|
Bot reviewVerdict: nothing blocking. The fix does what it says. The must-fix from the last review is fixed: no BlockingNone. Non-blocking
Checked and OK
|
Bot reviewFresh review of the new code (3be26ae). The leak from the last review is fixed. I found nothing that must change. Must-fixNone. Nits
Checked and OK
|
An address that is not in the mail, with more than 64 letters before the @, kept its first letters. Now each @ grows left and right over the address letters by hand. So the whole address is hidden, of any length, in one pass. The texts of the mail were searched one by one, so many recipients and a long error were slow. Now the error is put in lower case once, and read once. The texts are grouped by their first 3 letters and their length, so each place needs only a few set lookups. The address pass runs only when the error has an @. 5000 recipients and a 1 MB error now take about 60 to 200 ms. The docs now say that other Unicode forms may still show, and that header names and metadata keys are not hidden.
Fixer reply
Nit 5 (jsonDecode of invalid decrypted data) is left as is. Full |
- Cut an error to its first 16 KB before the redaction, and say it was cut. A huge error could block the worker for seconds. A mail text that goes past the cut is hidden up to the cut. - Check each place with a rolling hash before copying the text. Many mail texts with the same start were slow on a long error. - Hide the names of headers and the keys of metadata. A metadata key named tags, bytes or contentType no longer makes its value stay. - Export RedactedMailError from package:dox_queue/dox_queue.dart. - README: say which mail data can still show.
…to dox-queue - Mail.fake() has no default from, so the test example sets one. - The encrypt tip now says what the redacted error hides, what stays, and that a long error is cut to 16 KB. This matches dartondox/dox-queue#14.
Fixer replyCommit 78c1c5b. It is on top of ed31315 (the one-pass redaction), so it keeps that work.
Plain (not encrypted) mail jobs do not change. No version bump. Tests: |
Bot reviewFresh review of ed31315. The address scan and the fallback work. But I found three leaks in the text search and one speed problem. All of them are small fixes. Must-fix
Nits
Checked and OK
|
Hiding mail data in the error text by search left gaps each round: curly quotes, overlapping texts, and slow cases. Now the error that the worker logs and saves has only the error type and the SMTP or HTTP code. The full error text is encrypted with the app key, like the job data. At most 64 KB of it is encrypted, so a huge error stays small. RedactedMailError.decrypt() reads it back. When the error can not be encrypted, only the type and the code stay. The search code is removed.
Fixer replyDesign change, as asked. Text redaction is gone.
Tests:
|
Bot reviewVerdict: review-ok. Nothing blocking. I reviewed the whole PR (all 4 commits), not only the last one. What I checked
Non-blocking
|
Bot reviewFresh review of 026bd92 (the error is encrypted, not redacted). The code is good. I found no leak. One thing must change: the PR text. Must-fix
Nits
Checked and OK
|
Read the type and code of the error inside the try, so an error that
throws there can not leak its text.
decrypt() now only takes real encrypted details. It no longer gives
back plain text that starts with {.
The cut to 64K characters no longer splits an emoji in two. The answer
of the server (MailException.body) is now in the encrypted details too.
The docs now say 64K characters (UTF-16 code units), not 64 KB.
Fixer replyMust-fix: the PR body is rewritten for the encryption design (problem, effect, fix, tests). All old redaction text is gone. Nits (commit 413f040):
New tests: emoji at the cut, the server answer in the details, a throwing |
Bot reviewFresh review of 413f040, the whole PR. I found nothing that must change. Must-fixNone. Nits
Checked and OK
|
Bot reviewVerdict: review-ok. Nothing blocking. I reviewed the whole PR at 413f040, and I tested it myself. What 026bd92 and 413f040 do
BlockingNone. Non-blocking
Leak attempts (all safe)I wrote probe tests on a scratch copy (deleted after), worker on SQLite:
Checked and OK
|
Problem
With
Mail.queue(message, encrypt: true), the job data is saved encrypted. But when the send fails, the error text can name the recipient or show other mail data. For example an SMTP reply like550 <ann@example.com>: mailbox unavailable, or aMailExceptionthat names an address or the subject.Effect
The worker writes that error in plain text to its log (the warn line of each retry and the danger line of the last try) and to the failed jobs (the
errorcolumn offailed_jobs, or the failed job in Redis). Soencrypt: truestill leaked personal data.Fix
SendEncryptedQueuedMail.handle()catches the send error and throws aRedactedMailErrorwith the same stack trace (code lines only). Every place the worker writes the error gets this one.RedactedMailError: MailException (code 550). Details are encrypted: <base64>MailException.body(the answer of the server) when there is one. They are encrypted with the app key, like the job data (Queue.encryptData), bound to their own name, so job data can not be read as details.try. When reading or encrypting fails, only the type and the code stay (orunknownwhen the type can not be read). The original text is never written.RedactedMailError.decrypt(job.error)gives back the details, for aFailedJobfromQueue.driver.failed(). It needs the app key (or one ofAppConfig.previousKeys). It returns null when there are no details. It throwsDecryptExceptionfor a wrong key or for text that is not real encrypted details.RedactedMailErroris exported for this.encryptdo not change. Their data is in plain text in the queue anyway, and the full error helps to debug.Tests
test/redacted_mail_error_test.dart:decryptgives back the original text, also from a saved error with its stack trace.DecryptException. No details gives null.toString()throws, and one whoseruntimeTypethrows.SocketExceptionkeeps its type, with no code.test/queue_mail_test.dart(SQLite and Redis): through the worker withtries: 2, no mail data is in the log or in the failed jobs, anddecryptof the saved error gives the full SMTP reply. A 100 KB body quoted by the server does not show. A plain mail job keeps the full error.dart test: all pass (SQLite, and Redis on port 6459 with the test prefix).dart analyze --fatal-infosanddart format: clean.