| 1 | package home |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "sync" |
| 6 | |
| 7 | "modernc.org/sqlite" |
| 8 | ) |
| 9 | |
| 10 | // secureDeleteOnce guards the connection hook registration. The hook |
| 11 | // list is process-global — sqlite.RegisterConnectionHook appends to the |
| 12 | // package's single driver — so registering per call would run the pragma |
| 13 | // once per registration on every new connection, forever. |
| 14 | var secureDeleteOnce sync.Once |
| 15 | |
| 16 | // RegisterSecureDelete turns on PRAGMA secure_delete for every |
| 17 | // connection this process opens. |
| 18 | // |
| 19 | // It matters because deleting a row does not remove its bytes from the |
| 20 | // file (§5.1). What the pragma actually does, stated precisely because |
| 21 | // an earlier version of this comment overstated it: SQLite zeroes freed |
| 22 | // content in the pages it rewrites, so the page that lands in the main |
| 23 | // file after a checkpoint, and the frame written for that change, do not |
| 24 | // carry the deleted bytes. |
| 25 | // |
| 26 | // What it does NOT do: recall anything. A frame Litestream has already |
| 27 | // shipped is gone from our reach, and measurement confirms the deleted |
| 28 | // value is still readable in the local -wal until a checkpoint. The |
| 29 | // control narrows the window; it does not close it. That is why the |
| 30 | // column is sealed as well — the bytes that do survive are bytes nobody |
| 31 | // can read. |
| 32 | // |
| 33 | // Three things make this the shape it is, and each was a way to get it |
| 34 | // wrong: |
| 35 | // |
| 36 | // The pragma is PER CONNECTION. An Exec after opening sets it on |
| 37 | // whichever pooled connection served that call and on no other, and on |
| 38 | // none opened later. A test that reads the pragma back on that same |
| 39 | // connection passes while proving nothing. |
| 40 | // |
| 41 | // rastrillo/db.Open builds its own DSN — busy_timeout, journal_mode and |
| 42 | // foreign_keys — and passes nothing else through, so there is no way to |
| 43 | // add secure_delete as a DSN pragma without changing rastrillo. Hence |
| 44 | // the driver hook. |
| 45 | // |
| 46 | // It must be registered BEFORE db.Open. Open calls Ping, which opens the |
| 47 | // writer pool's single long-lived connection there and then; a hook |
| 48 | // registered afterwards misses that connection forever, and it is the |
| 49 | // only one that performs DELETEs. |
| 50 | // A neighbouring hazard, recorded here because this is where somebody |
| 51 | // will look: rastrillo/db.Open installs a GORM logger that prints slow |
| 52 | // queries — over 200ms — WITH THEIR PARAMETERS INTERPOLATED, at Warn. |
| 53 | // Nothing in this app sends an address through d.G today; everything |
| 54 | // address-bearing goes through Writer() with bound parameters. The first |
| 55 | // query that does go through GORM with an address in it will put that |
| 56 | // address into platform-held logs on any slow night. |
| 57 | func RegisterSecureDelete() { |
| 58 | secureDeleteOnce.Do(func() { |
| 59 | sqlite.RegisterConnectionHook(func(conn sqlite.ExecQuerierContext, _ string) error { |
| 60 | _, err := conn.ExecContext(context.Background(), "PRAGMA secure_delete=ON", nil) |
| 61 | return err |
| 62 | }) |
| 63 | }) |
| 64 | } |
| 65 | |