|
| 1 | +# Encrypt Personal Data |
| 2 | + |
| 3 | +As previously noted |
| 4 | +the schema created by the |
| 5 | +`phx.gen.auth` generator |
| 6 | +leaves **`email`** addresses stored |
| 7 | +as |
| 8 | +[**`plaintext`**](https://en.wikipedia.org/wiki/Plaintext): |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | +It's disappointing to us |
| 13 | +that this is the `default` |
| 14 | +and many devs |
| 15 | +will _naively_ think this is "OK". |
| 16 | + |
| 17 | +## It's _Never_ "OK" to Store Personal Data as `plaintext` |
| 18 | + |
| 19 | +It might be _tempting_ |
| 20 | +to store personal data as |
| 21 | +human-readable text |
| 22 | +during development |
| 23 | +to make it easier to debug, |
| 24 | +but we urge _everyone_ |
| 25 | +to resist this temptation! |
| 26 | +[Data breaches](https://en.wikipedia.org/wiki/Data_breach) |
| 27 | +happen _every_ day. |
| 28 | +Breaches can _destroy_ the reputation |
| 29 | +of a company. |
| 30 | +Thankfully, |
| 31 | +the EU now has stronger laws |
| 32 | +in the form of the |
| 33 | +[General Data Protection Regulation](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation) |
| 34 | +(GDPR), |
| 35 | +which carry heavy fines |
| 36 | +for companies |
| 37 | +that improperly store personal data. |
| 38 | + |
| 39 | +Most software engineers don't consider |
| 40 | +the law(s) around data protection. |
| 41 | +But none of us have _any_ excuse |
| 42 | +to ignore them even in basic projects. |
| 43 | + |
| 44 | +The good news is: |
| 45 | +protecting personal data |
| 46 | +is quite straightforward. |
| 47 | + |
| 48 | +## Why Encrypt _Now_? |
| 49 | + |
| 50 | +We _know_ this might feel like a deeply technical |
| 51 | +step to have so early on in |
| 52 | +the creation of the `auth` App. |
| 53 | +Why don't we just _skip_ this |
| 54 | +and focus on the interface `people` will _see_? |
| 55 | +We feel that getting the data privacy/security |
| 56 | +right from the _start_ |
| 57 | +is essential for building a robust |
| 58 | +and therefore trustworthy app. |
| 59 | +We will get to the interface design |
| 60 | +in the next chapter |
| 61 | +so if you prefer that part, |
| 62 | +feel free to speed-read/run this |
| 63 | +and only return to it when you |
| 64 | +need a deeper understanding. |
| 65 | + |
| 66 | + |
| 67 | +## How To Encrypt Sensitive Data? |
| 68 | + |
| 69 | +When we first started using `Elixir` in 2016 |
| 70 | +we explored the topic of |
| 71 | +encrypting personal data in _depth_ |
| 72 | +and wrote a comprehensive guide: |
| 73 | +[`dwyl/phoenix-ecto-encryption-example`](https://github.com/dwyl/phoenix-ecto-encryption-example) |
| 74 | +We _highly_ recommend following step-by-step guide |
| 75 | +to understand this in detail. |
| 76 | +We aren't going to duplicate/repeat |
| 77 | +any of the theory here. |
| 78 | +Rather we are going to focus on using |
| 79 | +the package we created |
| 80 | +[`fields`](https://github.com/dwyl/fields) |
| 81 | + |
| 82 | +## Using `Fields` to _Automatically_ Encrypt Personal Data |
| 83 | + |
| 84 | +Following the instructions in the |
| 85 | +[`fields`](https://github.com/dwyl/fields) |
| 86 | +repo, open the `mix.exs` file |
| 87 | +and locate the `defp deps do` section |
| 88 | +and add `fields` to the list: |
| 89 | + |
| 90 | +```elixir |
| 91 | +{:fields, "~> 2.10.3"}, |
| 92 | +``` |
| 93 | + |
| 94 | +Save the `mix.exs` file and run: |
| 95 | + |
| 96 | +```sh |
| 97 | +mix deps.get |
| 98 | +``` |
| 99 | + |
| 100 | + |
| 101 | +### Create Environment Variables |
| 102 | + |
| 103 | +`fields` expects an `environment variable |
| 104 | + |
| 105 | +e.g. using the [`phx.gen.secret`](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Secret.html) command: |
| 106 | + |
| 107 | +```sh |
| 108 | +mix phx.gen.secret |
| 109 | +``` |
| 110 | +You should see output similar to: |
| 111 | + |
| 112 | +```sh |
| 113 | +XGYVueuky4DT0s0ks2MPzHpucyl+9e/uY4UusEfgyR2qeNApzoYGSH+Y55cfDj1Y |
| 114 | +``` |
| 115 | + |
| 116 | +Export this key in your terminal with the following command: |
| 117 | + |
| 118 | +```sh |
| 119 | +export ENCRYPTION_KEYS=XGYVueuky4DT0s0ks2MPzHpucyl+9e/uY4UusEfgyR2qeNApzoYGSH+Y55cfDj1Y |
| 120 | +``` |
| 121 | + |
| 122 | +Run the `phx.gen.secret` command again |
| 123 | +and export it as `SECRET_KEY_BASE`, e.g: |
| 124 | + |
| 125 | +```sh |
| 126 | +export SECRET_KEY_BASE=GLH2S6EU0eZt+GSEmb5wEtonWO847hsQ9fck0APr4VgXEdp9EKfni2WO61z0DMOF |
| 127 | +``` |
| 128 | + |
| 129 | +We use an `.env` file on `localhost`: |
| 130 | + |
| 131 | +```sh |
| 132 | +export ENCRYPTION_KEYS=XGYVueuky4DT0s0ks2MPzHpucyl+9e/uY4UusEfgyR2qeNApzoYGSH+Y55cfDj1Y |
| 133 | +export SECRET_KEY_BASE=GLH2S6EU0eZt+GSEmb5wEtonWO847hsQ9fck0APr4VgXEdp9EKfni2WO61z0DMOF |
| 134 | +``` |
| 135 | + |
| 136 | +See: |
| 137 | +[`.env_sample`](https://github.com/dwyl/auth/blob/main/.env_sample) |
| 138 | +for a sample including |
| 139 | +all the recommended/required environment variables |
| 140 | +for running the `auth` app. |
| 141 | + |
| 142 | +Now to the _interesting_ part! |
| 143 | + |
| 144 | +## Update `people.email` Data Type |
| 145 | + |
| 146 | +The `phx.gen.auth` generator |
| 147 | +created the `people` schema |
| 148 | +with the `:email` field defined as a `:string`: |
| 149 | + |
| 150 | +```sh |
| 151 | +field :email, :string |
| 152 | +``` |
| 153 | + |
| 154 | +See: |
| 155 | +[`lib/auth/accounts/person.ex#L6`](https://github.com/dwyl/auth/blob/da0af7ee702c278714b47f92045edce4adad542a/lib/auth/accounts/person.ex#L6) |
| 156 | + |
| 157 | + |
| 158 | +### Create Migration File |
| 159 | + |
| 160 | +Using [`Ecto.Migration`](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#module-creating-your-first-migration) |
| 161 | +run the following command |
| 162 | +to create a migration file |
| 163 | +with a descriptive name: |
| 164 | + |
| 165 | +```sh |
| 166 | +mix ecto.gen.migration modify_people_email_string_binary |
| 167 | +``` |
| 168 | + |
| 169 | +You should see output similar to: |
| 170 | + |
| 171 | +```sh |
| 172 | +* creating priv/repo/migrations/20230309145958_modify_people_email_string_binary.exs |
| 173 | +``` |
| 174 | + |
| 175 | +Open the file in your editor. You should see: |
| 176 | + |
| 177 | +```elixir |
| 178 | +defmodule Auth.Repo.Migrations.ModifyPeopleEmailStringBinary do |
| 179 | + use Ecto.Migration |
| 180 | + |
| 181 | + def change do |
| 182 | + |
| 183 | + end |
| 184 | +end |
| 185 | +``` |
| 186 | + |
| 187 | +Update it to include the `alter` statement: |
| 188 | + |
| 189 | +```elixir |
| 190 | +defmodule Auth.Repo.Migrations.ModifyPeopleEmailStringBinary do |
| 191 | + use Ecto.Migration |
| 192 | + |
| 193 | + def change do |
| 194 | + alter table(:people) do |
| 195 | + remove :email |
| 196 | + add :email, :binary |
| 197 | + add ... |
| 198 | + end |
| 199 | + |
| 200 | + alter table(:people_tokens) do |
| 201 | + remove :sent_to |
| 202 | + add :sent_to, :binary |
| 203 | + end |
| 204 | + end |
| 205 | +end |
| 206 | +``` |
| 207 | + |
| 208 | +> **Note**: we tried using |
| 209 | +[`Ecto.Migration.modify/3`](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#modify/3) |
| 210 | +to modify the field type |
| 211 | +but got the error: |
| 212 | + |
| 213 | +```sh |
| 214 | +15:06:13.691 [info] alter table people |
| 215 | +** (Postgrex.Error) ERROR 42804 (datatype_mismatch) column "email" cannot be cast automatically to type bytea |
| 216 | + |
| 217 | + hint: You might need to specify "USING email::bytea". |
| 218 | +``` |
| 219 | + |
| 220 | +Given that this is a new project |
| 221 | +and there is no data in the DB, |
| 222 | +we decided it was easier |
| 223 | +to remove and re-add the `email` column/field. |
| 224 | + |
| 225 | + |
| 226 | +Save the migration file and run: |
| 227 | + |
| 228 | +```sh |
| 229 | +mix ecto.reset |
| 230 | +``` |
| 231 | + |
| 232 | +You should see output similar to the following: |
| 233 | + |
| 234 | +```sh |
| 235 | +15:13:29.989 [info] == Migrated 20230226095715 in 0.0s |
| 236 | + |
| 237 | +15:13:30.057 [info] == Running 20230309145958 Auth.Repo.Migrations.ModifyPeopleEmailStringBinary.change/0 forward |
| 238 | + |
| 239 | +15:13:30.058 [info] alter table people |
| 240 | + |
| 241 | +15:13:30.059 [info] alter table people_tokens |
| 242 | + |
| 243 | +15:13:30.059 [info] == Migrated 20230309145958 in 0.0s |
| 244 | +``` |
| 245 | + |
| 246 | +### Update `person.email` |
| 247 | + |
| 248 | +Open |
| 249 | +`lib/auth/accounts/person.ex` |
| 250 | +and replace the line: |
| 251 | + |
| 252 | +```elixir |
| 253 | +field :email, :string |
| 254 | +``` |
| 255 | + |
| 256 | +With: |
| 257 | + |
| 258 | +```elixir |
| 259 | +field :email, Fields.EmailEncrypted |
| 260 | +field :email_hash, Fields.EmailHash |
| 261 | +``` |
| 262 | + |
| 263 | +Sadly, this _essential_ privacy/security enhancement |
| 264 | +was not quite as straightforward as we had hoped |
| 265 | +and had quite a few ramifications. |
| 266 | +The boilerplate code |
| 267 | +generated by `phx.gen.auth` |
| 268 | +was relying on `person.email` being `plaintext` |
| 269 | +so we ended up having to make several changes. |
| 270 | + |
| 271 | +Rather than repeating all of these here |
| 272 | +which will take up a _lot_ of space |
| 273 | +and duplicate the code unessessarily, |
| 274 | +we recommend you read through the git commit: |
| 275 | +[`#2bbba99`](https://github.com/dwyl/auth/pull/231/commits/2bbba99c7057f0d2943dd8327be52ccf1a1bd58c) |
| 276 | + |
| 277 | +In a follow-up section |
| 278 | +we will be removing most of this code |
| 279 | +because we don't _want_ to |
| 280 | +use links in emails to verify `people`. |
| 281 | +Instead we will be using one-time-numeric codes. |
| 282 | + |
| 283 | + |
| 284 | +Following the changes made in: |
| 285 | +[`#2bbba99`](https://github.com/dwyl/auth/pull/231/commits/2bbba99c7057f0d2943dd8327be52ccf1a1bd58c) |
| 286 | + |
| 287 | +If we now run a query to view the data in the `people` table: |
| 288 | +```sql |
| 289 | +SELECT id, email, inserted_at FROM people; |
| 290 | +``` |
| 291 | +We see that the `email` is now a binary blob |
| 292 | +<img width="1096" alt="image" src="https://user-images.githubusercontent.com/194400/224109679-0084ae9d-a1e8-48f2-a083-94aff46d7137.png"> |
| 293 | + |
| 294 | +Registration and Login still works as expected: |
| 295 | +<img width="1159" alt="image" src="https://user-images.githubusercontent.com/194400/224111036-7a4e5764-8966-4b97-aaea-515c8e3b7fd4.png"> |
| 296 | + |
| 297 | +But now the personal data |
| 298 | +captured in registration is stored |
| 299 | +**`encrypted`** at rest |
| 300 | +the way it should be. 🔐 |
| 301 | + |
| 302 | +Now we can get on with _building_ `auth`! |
0 commit comments