├── .gitignore ├── LICENSE ├── README.md ├── creator ├── .gitignore ├── README.md ├── index.html ├── secret-template.html ├── serve.py └── values.js ├── examples ├── bounty.html ├── example-file.html ├── example-image.html └── example-message.html ├── images ├── cryptography.drawio └── cryptography.png └── notes.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Marco Primi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔐 Portable Secret 2 | 3 | Better privacy, without special software. 4 | 5 | ## TL;DR; 6 | 7 | Portable Secret is a little hack that allows you to: 8 | 9 | - Send encrypted messages/images/files over insecure channels (email, messaging, ...) 10 | - Store sensitive information (passwords, documents) in insecure locations (web, Cloud drives, USB drives) 11 | 12 | It's portable because: **decrypting these secrets does not require special software!** 13 | All you need is a browser. 14 | 15 | I created Portable Secret **to securely exchange documents via email with my mother**, who can't be expected to learn [PGP](https://en.wikipedia.org/wiki/Pretty_Good_Privacy), [age](https://github.com/FiloSottile/age), or similar. 16 | 17 | I also use Portable Secret to store some my most sensitive secrets (private keys, 2FA recovery codes, etc.) 18 | 19 | Finally, I use it to store copies of important documents (like a picture of passport). These documents are accessible to me from anywhere, **even if all my trusted devices have been stolen or lost**. 20 | 21 | Sounds too good to be true? Keep reading. This is for you. 22 | 23 | --- 24 | 25 | Portable Secret is not a product and it is barely a project. 26 | **It is just a neat trick, a *hack***. 27 | 28 | The [source code](https://github.com/mprimi/portable-secret) and [creator tool](https://mprimi.github.io/portable-secret/creator/) are provided as a demonstration. 29 | 30 | **Update January 2023**: [Rocky W.](https://www.rocky.dev/) took this idea and ran with it, creating a beautiful full-fledged product: [PrivacyProtect.dev](https://www.privacyprotect.dev/) 31 | 32 | --- 33 | 34 | ## How it works 35 | 36 | A 'Portable Secret' is simply an HTML file that also contains: 37 | - An encrypted payload 38 | - Some Javascript that calls into the browser's [Web Cryptography APIs](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) 39 | 40 | Any (reasonably modern) web browser can open the file, even without an internet connection! 41 | If you know the password, you can recover the secret within. 42 | 43 | [Here's an example](https://mprimi.github.io/portable-secret/examples/example-message.html) (the password is `banana`) 44 | 45 | Notice that the file is self-contained and has no external dependencies! 46 | It can be carried on a USB drive and decrypted without an internet connection, on any device that has a web browser. 47 | 48 | To understand how it works, go ahead and [create yourself a secret](https://mprimi.github.io/portable-secret/creator/). Download and inspect the generated Portable Secret. 49 | 50 | The embedded code is straightforward: 51 | - Take the password and generate a key 52 | - Use the key to decrypt the payload 53 | - Display the decrypted secret 54 | 55 | --- 56 | 57 | ## How I use Portable Secret 58 | 59 | ### Private communication 60 | 61 | Do you want to communicate privately with people, but you can't expect them to learn how to use PGP? 62 | 63 | Send a Portable Secret. For example as email attachment. 64 | 65 | > Hey ___, attached to this email is the PDF and data you requested, but it's encrypted. Never double-click on attachments, it's dangerous! Give me a call when you get this, and I'll show you how to read it. 66 | 67 | Whenever they call me, I tell them: 68 | > Right-click on the attachment and 'Open with...' any browser. 69 | > The password is 'banana_split'. 70 | > Now you can save the decrypted PDF. 71 | 72 | ### Store top-level secrets 73 | 74 | Some secrets don't belong in your password manager. Things like backup private keys, 2FS recovery keys, wallet keys, safe combinations, treasure maps, etc. 75 | 76 | Using Portable Secret, I can keep copies of these critical keys all over the place (Cloud drives, USB drives, all my devices, etc). 77 | 78 | Even if some of these copies end up stolen (e.g. I lose a USB stick), I am not concerned anyone will be able to recover the secrets within. 79 | 80 | The passwords are long sequence of words that are trivial for me to remember (thanks to the hints provided), but impossible for anyone else to guess or crack. 81 | 82 | ### Emergency documents on the go 83 | 84 | Have you ever gotten stranded in a foreign country without any of your devices or documents? It's not fun. 85 | 86 | I keep a copy of my passport encrypted on the internet. It's just an HTML file, it's easy to host. If I find myself stranded again, I can use any computer/device to retrieve it. 87 | 88 | [Here's an example of ID document safely encrypted in plain sight](https://mprimi.github.io/portable-secret/examples/example-image.html). 89 | 90 | --- 91 | 92 | ## Cryptography 93 | 94 | The following is a schematic representation of the encryption scheme implemented by the PortableSecret creator, as well as the decryption happening in the PortableSecret itself. 95 | 96 | ![Cryptography](./images/cryptography.png) 97 | 98 | This scheme and its parameters follow best practices and guidelines recommended by [NIST](https://www.nist.gov/cryptography) and [OWASP](https://owasp.org). 99 | 100 | ### Encryption 101 | 102 | [`AES-GCM`](https://en.wikipedia.org/wiki/Galois/Counter_Mode) is used for Symmetric Authenticated Encryption. 103 | 104 | - Symmetric because the same key is used to encrypt and decrypt 105 | - *Authenticated* because it can verify the message **integrity** (if the ciphertext is tampered with, then it will fail to decrypt. As opposed to producing an invalid plaintext) 106 | 107 | ### Key derivation 108 | 109 | [`PBKDF2`](https://en.wikipedia.org/wiki/PBKDF2) is used to turn a text password into an AES key. 110 | The purpose of this module is slowing down dictionary-based brute-force attacks. 111 | 112 | Unfortunately, the Web Crypto APIs do not support stronger KDFs such as [`scrypt`](https://en.wikipedia.org/wiki/Scrypt), [`bcrypt`](https://en.wikipedia.org/wiki/Bcrypt), or [Argon2](https://en.wikipedia.org/wiki/Argon2). 113 | 114 | --- 115 | 116 | ## Bounty: Crack me if you can 117 | 118 | Do you think this cannot possibly be secure? Great, prove it. 119 | 120 | [This secret](https://mprimi.github.io/portable-secret/examples/bounty.html) contains the recovery key for a Bitcoin wallet. Crack it and take my money! 121 | 122 | --- 123 | 124 | ## Miscellaneous 125 | 126 | ### Choosing a good password 127 | 128 | Choosing a strong-enough password is key (pun intended). 129 | 130 | Eventually I'll fill in this paragraph. For now all you get is the obligatory XKCD: [correct-horse-battery-staple](https://xkcd.com/936/) 131 | 132 | ### On tools 133 | 134 | Portable Secret is a *tool*. As such, it can be used *wrong* (e.g. weak password), or used to do bad things (e.g., exfiltrate intellectual property). 135 | 136 | I cannot take responsibility for such misuse any more than a hammer manufacturer can take responsibility for me hammering my thumb, or using the hammer to attack someone. 137 | 138 | ### Prior art 139 | 140 | I came up with Portable Secret on my own, but I have since found a few projects that do something similar. 141 | 142 | [StatiCrypt](https://github.com/robinmoisson/staticrypt) 143 | 144 | [PolySafe](https://github.com/fmeum/polysafe) 145 | 146 | [`hscrypt`](https://smondet.gitlab.io/hscrypt/) 147 | 148 | [Hypervault](http://hypervault.github.io) 149 | 150 | [Encrypted HTML Vault](https://github.com/ccorcos/encrypted-html-vault) 151 | 152 | [UltraCrypt](https://9p4.github.io/hackna/) 153 | 154 | [html-vault](https://github.com/dividuum/html-vault) 155 | 156 | [Password Protect My File](https://github.com/louissobel/ppmf) 157 | 158 | [Emergency Contacts](https://github.com/jwillmer/emergency-contacts) 159 | 160 | [Digi-Cloak](https://github.com/kaushalmeena/digi-cloak) 161 | 162 | [Pretty Easy Privacy](https://prettyeasyprivacy.xyz/) 163 | 164 | If you are aware of other similar/related projects, please let me know and I'll link them here. 165 | 166 | ### Feedback 167 | 168 | I would love to hear what you think of this project, good, bad, or ugly. 169 | 170 | Please use [GH issue](https://github.com/mprimi/portable-secret/issues) to report a problems and make suggestions. For everything else, start a [GH Discussion](https://github.com/mprimi/portable-secret/discussions). 171 | 172 | You can also find my email on my homepage (linked from my GH profile). 173 | 174 | Or discuss on [HackerNews](https://news.ycombinator.com/item?id=34083366) 175 | 176 | ## References 177 | 178 | https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API 179 | 180 | https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html 181 | 182 | https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html 183 | 184 | https://en.wikipedia.org/wiki/Galois/Counter_Mode 185 | 186 | https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf 187 | 188 | https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs 189 | -------------------------------------------------------------------------------- /creator/.gitignore: -------------------------------------------------------------------------------- 1 | cert.crt 2 | private.key 3 | -------------------------------------------------------------------------------- /creator/README.md: -------------------------------------------------------------------------------- 1 | # Portable Secret Creator 2 | 3 | Creator could be embedded in a single HTML file with inlined Javascript, and be portable. 4 | 5 | However this has some issues: 6 | - Inlining all JS requires 2 levels of escaping, making it hard to modify/maintain those files 7 | - Some browsers disable `window.crypto` on local files and non-TLS servers 8 | 9 | So the easiest way to run the creator locally is: 10 | 11 | ``` 12 | openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out cert.crt -keyout private.key 13 | 14 | python3 serve.py 15 | ``` 16 | 17 | Then visit https://127.0.0.1:8443/index.html 18 | 19 | Notice that some browsers are not happy with the self-signed certificate. 20 | 21 | All of this is just for local development. 22 | You can use the creator at: https://mprimi.github.io/portable-secret/creator/ or host it on your own (HTTPS) server. 23 | -------------------------------------------------------------------------------- /creator/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 35 | 322 | 323 | 324 |

Portable Secret: Secret Creator

325 |

326 | This tool runs entirely in your browser window. The secret never leaves your computer!
327 | But don't take my word for it. Check out the source code! 328 |

329 | 330 |
331 | Password:
332 | 333 |
334 | 335 | 336 |
337 | Password hint:
338 | 340 |
341 | 342 | 343 | 344 |

Secret type:

345 |
346 | 347 | 348 | 349 | 350 | 351 | 352 |
353 | 354 | 355 | 356 | 360 | 364 | 368 | 369 | 370 | 371 |

372 |

373 | 374 | 375 |
376 |
377 | 378 |
379 | 380 | 381 | 382 |
383 |
384 | Show more 385 |

⚠️ Don't modify these settings unless you know what you are doing

386 |
387 | Salt: 0x 388 | 389 | 390 |
391 | IV: 0x 392 | 393 | 394 |

Salt and IV are random input coming straight from your browser's Random Number Generator. Do not reuse across messages.

395 |
396 |
397 | PBKDF2 iterations: 398 | 399 | 400 |

Higher iteration count slow down key generation, making dictionary-based attack harder.

401 |
402 |
403 | Ciphertext: 404 |
405 | 406 |
407 |
408 | 409 | 410 | -------------------------------------------------------------------------------- /creator/secret-template.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 69 | 222 | 223 | 224 | 225 |

This page contains a secret

226 |

Enter the password to decrypt it

227 |

Created with Portable Secret

228 | 229 |

230 | This file contains a secret (message, file, or image) that can be recovered if you know the password.
231 | The secret can be decrypted without an internet connection, this file has no dependencies and no data leaves the browser window. 232 |

233 | 234 |
235 |

Password hint:

236 |
{{PASSWORD_HINT}}
237 |
238 | 239 |
240 |

Password:

241 | 242 |
243 | 244 |
245 | 246 | 247 |
248 | 249 | 252 | 253 | 256 | 257 | 260 | 261 |
262 | Details 263 | 264 | These are decryption inputs, that can be safely transmitted in the clear. 265 | Without the correct password, they are useless. 266 | 267 |
268 | Salt: 269 | 270 |
271 | 272 |
273 | IV: 274 | 275 |
276 | 277 |
278 | Ciphertext:
279 | 280 |
281 |
282 | 283 | 286 | 287 | -------------------------------------------------------------------------------- /creator/serve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | 4 | # Modified version of: 5 | # https://gist.github.com/SeanPesce/af5f6b7665305b4c45941634ff725b7a 6 | 7 | import http.server 8 | import ssl 9 | import sys 10 | 11 | host = "0.0.0.0" 12 | port = 8443 13 | # See README in this directory for instructions to generate certificate and key 14 | cert = "./cert.crt" 15 | key = "./private.key" 16 | 17 | if __name__ == '__main__': 18 | context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 19 | context.load_cert_chain(certfile=cert, keyfile=key, password='') 20 | server_address = (host, port) 21 | httpd = http.server.HTTPServer(server_address, http.server.SimpleHTTPRequestHandler) 22 | httpd.socket = context.wrap_socket(httpd.socket, server_side=True) 23 | print(f"https://localhost:{port}") 24 | httpd.serve_forever() 25 | -------------------------------------------------------------------------------- /creator/values.js: -------------------------------------------------------------------------------- 1 | const secretType = "'{.SECRET_TYPE}'" 2 | const secretExt = "'{.SECRET_EXTENSION}'" 3 | const saltSize = '{.SALT_SIZE}' // bytes 4 | const blockSize = '{.BLOCK_SIZE}' // bytes 5 | const keySize = '{.KEY_SIZE}' // bytes 6 | const iterations = '{.ITERATIONS}' 7 | const saltHex = "'{.SALT_HEX}'" 8 | const ivHex = "'{.IV_HEX}'" 9 | const cipherHex = "'{.CIPHER_HEX}'" 10 | -------------------------------------------------------------------------------- /examples/bounty.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 69 | 222 | 223 | 224 | 225 |

Portable Secret Bounty

226 |

This secret contains a Bitcoin wallet recovery key

227 |

If you can crack the secret, the funds are yours! (~$400 at today's rate)

228 | 229 |

230 | You can check the status of the wallet here: here. 231 |

232 | 233 |
234 |

Password hint:

235 |
4 words, joined by dots, capitalized as necessary
236 | 
237 | 1. The swimming pool accident -- her first name
238 | 2. Name of the flowers on the balcony that I used to care for
239 | 3. Name of the guy that really made a difference (The Encounter, 2017)
240 | 4. The gift from Veronica (2 words without space)
241 | 
242 |
243 | 244 |
245 |

Password:

246 | 247 |
248 | 249 |
250 | 251 | 252 |
253 | 254 | 257 | 258 | 261 | 262 | 265 | 266 |
267 | Details 268 | 269 | These are decryption inputs, that can be safely transmitted in the clear. 270 | Without the correct password, they are useless. 271 | 272 |
273 | Salt: 274 | 275 |
276 | 277 |
278 | IV: 279 | 280 |
281 | 282 |
283 | Ciphertext:
284 | 285 |
286 |
287 | 288 | 299 | 300 | -------------------------------------------------------------------------------- /examples/example-file.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 69 | 222 | 223 | 224 | 225 |

This page contains a secret

226 |

Enter the password to decrypt it

227 |

Created with Portable Secret

228 | 229 |

230 | This file contains a secret (message, file, or image) that can be recovered if you know the password.
231 | The secret can be decrypted without an internet connection, this file has no dependencies and no data leaves the browser window. 232 |

233 | 234 |
235 |

Password hint:

236 |
A yellow edible fruit with elongated shape. 6 letters, all lowercase
237 | 
238 | (N.B. this is a _weak_ password. But for this example it will do)
239 | 
240 |
241 | 242 |
243 |

Password:

244 | 245 |
246 | 247 |
248 | 249 | 250 |
251 | 252 | 255 | 256 | 259 | 260 | 263 | 264 |
265 | Details 266 | 267 | These are decryption inputs, that can be safely transmitted in the clear. 268 | Without the correct password, they are useless. 269 | 270 |
271 | Salt: 272 | 273 |
274 | 275 |
276 | IV: 277 | 278 |
279 | 280 |
281 | Ciphertext:
282 | 283 |
284 |
285 | 286 | 297 | 298 | -------------------------------------------------------------------------------- /examples/example-image.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 69 | 221 | 222 | 223 | 224 |

This page contains a secret

225 |

Enter the password to decrypt it

226 |

Created with Portable Secret

227 | 228 |

229 | This file contains a secret (message, file, or image) that can be recovered if you know the password.
230 | The secret can be decrypted without an internet connection, this file has no dependencies and no data leaves the browser window. 231 |

232 | 233 |
234 |

Password hint:

235 |
A yellow edible fruit with elongated shape. 6 letters, all lowercase
236 | 
237 | (N.B. this is a _weak_ password. But for this example it will do)
238 | 
239 |
240 | 241 |
242 |

Password:

243 | 244 |
245 | 246 |
247 | 248 | 249 |
250 | 251 | 254 | 255 | 258 | 259 | 262 | 263 |
264 | Details 265 | 266 | These are decryption inputs, that can be safely transmitted in the clear. 267 | Without the correct password, they are useless. 268 | 269 |
270 | Salt: 271 | 272 |
273 | 274 |
275 | IV: 276 | 277 |
278 | 279 |
280 | Ciphertext:
281 | 282 |
283 |
284 | 285 | 296 | 297 | -------------------------------------------------------------------------------- /examples/example-message.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 69 | 221 | 222 | 223 | 224 |

This page contains a secret

225 |

Enter the password to decrypt it

226 |

Created with Portable Secret

227 | 228 |

229 | This file contains a secret (message, file, or image) that can be recovered if you know the password.
230 | The secret can be decrypted without an internet connection, this file has no dependencies and no data leaves the browser window. 231 |

232 | 233 |
234 |

Password hint:

235 |
A yellow edible fruit with elongated shape. 6 letters, all lowercase
236 | 
237 | (N.B. this is a _weak_ password. But for this example it will do)
238 |
239 | 240 |
241 |

Password:

242 | 243 |
244 | 245 |
246 | 247 | 248 |
249 | 250 | 253 | 254 | 257 | 258 | 261 | 262 |
263 | Details 264 | 265 | These are decryption inputs, that can be safely transmitted in the clear. 266 | Without the correct password, they are useless. 267 | 268 |
269 | Salt: 270 | 271 |
272 | 273 |
274 | IV: 275 | 276 |
277 | 278 |
279 | Ciphertext:
280 | 281 |
282 |
283 | 284 | 295 | 296 | -------------------------------------------------------------------------------- /images/cryptography.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /images/cryptography.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mprimi/portable-secret/60ad4ca81b9851aeeead719082e25730f82e0a38/images/cryptography.png -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | https://www.w3.org/TR/WebCryptoAPI/#aes-gcm 2 | 3 | https://en.wikipedia.org/wiki/Galois/Counter_Mode 4 | 5 | https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf 6 | 7 | https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API 8 | 9 | https://base64.guru/developers/javascript/examples/unicode-strings 10 | 11 | https://developer.mozilla.org/en-US/docs/Glossary/Base64 12 | 13 | https://stackoverflow.com/questions/23223718/failed-to-execute-btoa-on-window-the-string-to-be-encoded-contains-characte 14 | 15 | https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs 16 | --------------------------------------------------------------------------------