$ cat privacy.md
Information flow control
A label on a field is the whole privacy policy. The compiler carries it everywhere the value goes and refuses every path to a page, an attribute, a branch, a loop or a log line. The only way across is explicit, and every crossing is listed.
$ cat model.complex
model Citizen {
name: Text
city: Text
email: Text @personal
bsn: Text @secret
}Three labels, ordered: public < personal < secret. Anything computed from labelled data carries the highest label of its inputs. A secret can only leave through masked(); personal data only through reveal().
$ complex check leak.complex
This does not compile. Four leaks, four errors, each with a line number and a fix:
page "/citizen/:id" (id: Int) {
let c = Citizen[id]
log "viewed " + c.email // error: Personal<Text> cannot be written to the log
view {
p { c.bsn } // error: Secret<Text> cannot be shown in a view
if c.bsn == "999990019" { } // error: cannot branch on secret data
a(href: "/x?e=" + c.email) { } // error: Personal<Text> cannot be placed in 'href'
}
}$ cat fixed.complex
page "/citizen/:id" (id: Int) {
let c = Citizen[id]
log "viewed citizen " + id
view {
p { c.name }
p { masked(c.bsn, 3) } // ok: explicit, and listed by `complex audit`
p { reveal(c.email) } // ok: explicit, and listed by `complex audit`
}
}$ complex audit site
The audit is not a document someone wrote. It is derived from the source by the compiler, so it cannot drift from what the code does:
$ complex audit site
labelled data
Citizen.email personal
Citizen.bsn secret
declassifications (2)
privacy.complex:62:21 masked() secret -> public
p { masked(c.bsn, 3) }
privacy.complex:63:21 reveal() personal -> public
p { reveal(c.email) }
result: no labelled value reaches a sink except through the declassifications above$ ls /citizen
Live, from this very model:
- A. Voorbeeld · Hattem
- B. Demo · Kampen
- C. Fictief · Zwolle
Or by id: /citizen/1, /citizen/2, /citizen/3. Try /citizen/9 for a 404.