├── LICENSE ├── README.md ├── SUMMARY.md ├── package.json ├── problems ├── 01.md ├── 02.md ├── 03.md ├── 04.md ├── 05.md ├── 06.md ├── 07.md ├── 08.md ├── 09.md ├── 10.md ├── 11.md ├── 12.md ├── 13.md ├── 14.md ├── 15.md ├── README.md └── extra-credit.md └── solutions ├── 01 ├── bank.js └── teller.js ├── 02 ├── bank.js └── teller.js ├── bank.js └── teller.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Emil Bay & Mathias Buus 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `learntocrypto` 2 | 3 | > Learn to crypto workshop 4 | 5 | ## Usage 6 | 7 | Learn cryptographic engineering through a set of exercises. 8 | 9 | [**Start here: `problems/README.md`**](problems/README.md) 10 | 11 | ## Install 12 | 13 | ```sh 14 | git clone git://github.com/sodium-friends/learntocrypto.git 15 | ``` 16 | 17 | ## License 18 | 19 | [ISC](LICENSE) 20 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | 1. [Banking Scaffold]() - RPC 4 | x. [Deposit operation]() 5 | x. [Withdrawal operation]() 6 | x. [Balance operation]() 7 | x. [Persitance / Replay]() 8 | x. [Threat model]() - transaction integrity 9 | 2. [Hash chain]() 10 | x. [Verify integrity]() 11 | 12 | 3. [Updated threat model]() 13 | x. [Signed hash chain]() 14 | x. [A word about key-pairs]() 15 | 16 | 4. [Adding customer accounts]() => Accounts by Acc # 17 | x. [map/reduce]() 18 | x. [Customer key-pairs]() - Bank binds PK to acc => certificate authority 19 | x. [Customer signed hash chain]() - Co-signed by both customer and bank = Non-repudiation 20 | 21 | 5. [Latest hash as receipt]() - Replay attack, distributed sync, local (customer) vs global (bank) hash 22 | x. [Updated integrity check]() 23 | 24 | 5. [At-rest encryption]() - encryption at rest 25 | x. [Encrypt key-pair] 26 | x. [Encrypt transaction log] 27 | 28 | 29 | 30 | 31 | 6. [Key management]() 32 | 7. [Encrypted connection]() 33 | 34 | 35 | X. [Beefing up security]() - Secure memory, memory locking 36 | X. [Deriving symmetric key from asymmetric]() 37 | X. [Random data / entropy] 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learntocrypto", 3 | "version": "0.0.0", 4 | "description": "Learn to crypto workshop", 5 | "main": "index.js", 6 | "dependencies": { 7 | "duplex-json-stream": "^1.0.1", 8 | "siginfo": "^1.0.1", 9 | "sodium-native": "^2.0.0" 10 | }, 11 | "devDependencies": {}, 12 | "scripts": {}, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/sodium-friends/learntocrypto.git" 16 | }, 17 | "keywords": [], 18 | "author": "Emil Bay ", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/sodium-friends/learntocrypto/issues" 22 | }, 23 | "homepage": "https://github.com/sodium-friends/learntocrypto#readme" 24 | } 25 | -------------------------------------------------------------------------------- /problems/01.md: -------------------------------------------------------------------------------- 1 | # 01. The bank interface 2 | 3 | We will first be building a banking server and a teller client. The banking 4 | server represents the bank vault, and the client is what tellers use to 5 | deposit and withdraw funds from the vault. But we're already getting ahead of 6 | ourselves. Let's start with the general bank scaffold. 7 | 8 | ## Server 9 | 10 | To keep everything simple we will be building a simple TCP server that 11 | understands JSON objects, using the core `net` module and `duplex-json-stream`: 12 | 13 | ```js 14 | // bank.js 15 | var jsonStream = require('duplex-json-stream') 16 | var net = require('net') 17 | 18 | var server = net.createServer(function (socket) { 19 | socket = jsonStream(socket) 20 | 21 | socket.on('data', function (msg) { 22 | console.log('Bank received:', msg) 23 | // socket.write can be used to send a reply 24 | }) 25 | }) 26 | 27 | server.listen(3876) 28 | ``` 29 | 30 | ## Client 31 | 32 | To interact with our bank, we will need a client that talks the same protocol. 33 | The server and client are asymmetric, just like in HTTP, so our clients will be 34 | ephemeral, while our server is where state lives. 35 | 36 | ```js 37 | // teller.js 38 | var jsonStream = require('duplex-json-stream') 39 | var net = require('net') 40 | 41 | var client = jsonStream(net.connect(3876)) 42 | 43 | client.on('data', function (msg) { 44 | console.log('Teller received:', msg) 45 | }) 46 | 47 | // client.end can be used to send a request and close the socket 48 | ``` 49 | 50 | ## Problem 51 | 52 | First extend the client to send `{cmd: 'balance'}` to the bank, and have the 53 | bank reply with `{cmd: 'balance', balance: 0}` 54 | 55 | ## Testing 56 | 57 | To test your application, open two terminals and run `node bank.js` in one 58 | session and run `node teller.js` in the other. The teller should print out 59 | `Teller received: {cmd: 'balance', balance: 0}` and exit, while the bank should 60 | print `Bank received: {cmd: 'balance'}` and keep running. 61 | 62 | [Well done, continue to problem 02](02.md) 63 | -------------------------------------------------------------------------------- /problems/02.md: -------------------------------------------------------------------------------- 1 | # 02. Depositing schmeckels 2 | 3 | Congratulations on passing problem 01. As you probably noticed the bank so far 4 | doesn't do much, so let's try and expand the functionality a bit. Pinky promise we'll 5 | get to the crypto soon! 6 | 7 |
8 | Solution to problem 01 9 | 10 | ```js 11 | // bank.js 12 | var jsonStream = require('duplex-json-stream') 13 | var net = require('net') 14 | 15 | var server = net.createServer(function (socket) { 16 | socket = jsonStream(socket) 17 | 18 | socket.on('data', function (msg) { 19 | console.log('Bank received:', msg) 20 | socket.write({cmd: 'balance', balance: 0}) 21 | }) 22 | }) 23 | 24 | server.listen(3876) 25 | ``` 26 | 27 | ```js 28 | // teller.js 29 | var jsonStream = require('duplex-json-stream') 30 | var net = require('net') 31 | 32 | var client = jsonStream(net.connect(3876)) 33 | 34 | client.on('data', function (msg) { 35 | console.log('Teller received:', msg) 36 | }) 37 | 38 | client.end({cmd: 'balance'}) 39 | ``` 40 | 41 |
42 | 43 | ## Tracking transactions 44 | 45 | Next you should add a `deposit` command, which takes an `amount` and adds this 46 | to the bank vault balance. But to keep an audit trail all deposits should be 47 | kept in a transaction log. This will prove to have lots of benefits later. A 48 | transaction log is just another way of saying that all operations that modify 49 | the state are kept in an array, and the current state is derived by `.reduce`ing 50 | this array. 51 | 52 | For example, here's a transaction log that should reduce to balance of 53 | 250 schmeckels: 54 | 55 | ```js 56 | var log = [ 57 | {cmd: 'deposit', amount: 130}, 58 | {cmd: 'deposit', amount: 0}, 59 | {cmd: 'deposit', amount: 120} 60 | ] 61 | ``` 62 | 63 | ### Side quest 64 | 65 | As an optional exercise, try writing a [`.reduce`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) 66 | function that takes the above log, and arrives at the correct balance. To test 67 | it out, try adding more entries to the log. 68 | 69 | ## Problem 70 | 71 | You job now is to expand the `teller.js` and `bank.js` with the `deposit` 72 | command, that is stored in a transaction log and updates the bank state (ie. 73 | it's balance). When the bank gets a deposit command, it should reply with the 74 | current balance like this: `{cmd: 'balance', balance: someNumber}`. 75 | A good idea is to make `teller.js` a very simple CLI tool, reading commands and 76 | arguments from `process.argv`. 77 | 78 |
79 | Hint 80 | 81 | You can easily handle multiple commands using a switch statement like this: 82 | 83 | ```js 84 | switch (command) { 85 | case 'balance': 86 | // ... 87 | break 88 | 89 | case 'deposit': 90 | // ... 91 | break 92 | 93 | default: 94 | // Unknown command 95 | break 96 | } 97 | ``` 98 | 99 |
100 | 101 | ## Testing 102 | 103 | Spin up your new `bank.js` to make a couple of deposit up to 250 schmeckels in 104 | your bank, using `node teller.js deposit 123`, and checking your balance. 105 | 106 | [Continue to problem 03](03.md) 107 | -------------------------------------------------------------------------------- /problems/03.md: -------------------------------------------------------------------------------- 1 | # 03. Withdrawing schmeckels 2 | 3 | Not much fun depositing schmeckels with your bank if you can't retrieve them 4 | again! The next task is simply extending the bank, teller and transaction log 5 | with a `withdraw` command. 6 | 7 |
8 | Solution to problem 02 9 | 10 | ```js 11 | // bank.js 12 | 13 | ``` 14 | 15 | ```js 16 | // teller.js 17 | 18 | ``` 19 | 20 |
21 | 22 | ## Problem 23 | 24 | Add a `withdraw` command, that `.push`es this new entry to the transaction log 25 | and handle the command type when calculating your balance. Remember to check for 26 | sufficient funds (unless your government does bailouts). Also add a new command 27 | to your teller client. 28 | 29 | ## Testing 30 | 31 | Restart your `bank.js` and try making a couple of deposits and withdrawals. You 32 | should try and withdraw more money than is available in the bank, and have your 33 | command be refused. 34 | 35 | [Continue to problem 04](04.md) 36 | -------------------------------------------------------------------------------- /problems/04.md: -------------------------------------------------------------------------------- 1 | # 04. Persistence 2 | 3 | Having our bank lose all transactions between restarts is not only annoying, 4 | but also bad for operations and business. 100% uptime is not a bet we want to 5 | make in our bank. 6 | 7 | ## Problem 8 | 9 | Persisting the transaction log to disk is as easy as calling `JSON.stringify` on 10 | our transaction log and using the core `fs` module to write the string to disk. 11 | Loading the transaction log back in is just using `require` on the resulting 12 | JSON file. Don't over-think things here, there's heaps of way this can be 13 | optimised and there are many sequencing problems here that you can spend the 14 | rest of the week ironing out, but that's not why we're here. They are good to 15 | keep in mind and note, though. Here's some of the ones we've thought of: race 16 | conditions with multiple tellers making operations at the same time, writing 17 | only new records to disk, only replying to the teller after the log has been 18 | committed to disk, etc. 19 | 20 |
21 | Hint 22 | 23 | You can take advantage of the 2nd argument of 24 | `JSON.stringify(obj, null, indent)` to get pretty printing of your JSON file. 25 | This will make debugging and some of the following exercises easier. 26 |
27 | 28 | ## Testing 29 | 30 | Test that your `bank.js` produces a transaction JSON file by issuing it a couple 31 | of `deposit` and `withdraw` commands. Also check that the balance is the same 32 | before and after restarting. 33 | 34 | [Continue to problem 05](05.md) 35 | -------------------------------------------------------------------------------- /problems/05.md: -------------------------------------------------------------------------------- 1 | # 05. Threat modelling 2 | 3 | Now we have a functioning bank! A very simple one, but a functional one. But it is also completely insecure! We put our complete trust in the tellers, the 4 | system administrators and directors at the bank. So let's start thinking of how 5 | we can secure our bank. 6 | 7 |
8 | Solution to problem 04 9 | 10 | ```js 11 | // bank.js 12 | 13 | ``` 14 | 15 | ```js 16 | // teller.js 17 | 18 | ``` 19 | 20 |
21 | 22 | ## How to define threat models 23 | 24 | One of the important rituals in engineering secure systems is threat modelling. 25 | It is more of a planning exercise than a practical one. It is all about 26 | defining who your opponents (called adversaries in crypto-lingo) are, what they 27 | stand to gain from breaking your system and how we can either eliminate the 28 | threat they pose or make it so costly to achieve their goals, that their 29 | threat becomes negligible in your model. 30 | 31 | Threat modelling helps you focus on real threats instead of contrived ones. If 32 | you do not keep this laser focus on what is actually within your model, you will 33 | be stuck in "paralysis analysis". This is also why it is important to figure out 34 | what your adversary stands to gain, and how far they are willing to go to break 35 | your system, plus what you're willing to invest to mitigate the risk. 36 | 37 | 38 | ## Our first threat model 39 | 40 | So far we have have only briefly mentioned two security aspects; a transaction 41 | log which also serves as a simple audit trail and checking that the bank has 42 | sufficient funds. The latter you might not regards as a security feature, but 43 | being extremely cautious and only accepting well defined inputs is the first 44 | step towards securing the system. It is also important here to realise that it 45 | is the `bank.js` server that should be locked down, while any checking in the 46 | `teller.js` is just "nice" validation to save a network round-trip. 47 | 48 | Validating inputs is, as such, not important to our tour of cryptography, so we 49 | will leave that as a bonus exercise. The transaction log, however, is at the core 50 | of our bank. As it stands now, anyone can reorder, add or remove 51 | transactions, without us being able to detect it. Securing our transaction log 52 | will be our first avenue into cryptography. A first threat model may look 53 | roughly like the following: 54 | 55 | * **Who**: Anyone with physical access to the bank (disgruntled staff, corrupt directors, evil hackers) 56 | * **What**: Transaction forgery 57 | * **How**: Simply editing the transaction log 58 | 59 | ## Testing 60 | 61 | Unleash your inner hacker! (or script-kiddie.) Try reordering, adding and 62 | removing items from the log. Depending on your logic, you may even be able to 63 | put the bank into a deficit, despite that being something that should not be 64 | possible. We're in big trouble now. 65 | 66 | [Continue to problem 06](06.md) 67 | -------------------------------------------------------------------------------- /problems/06.md: -------------------------------------------------------------------------------- 1 | # 06. Our first crypto primitive (hashing) 2 | 3 | Now that we have a running bank and a threat model with a high risk target, 4 | let's look at what we can do to mitigate this risk. As mentioned previously, 5 | in this workshop we will be using the cryptographic library called `libsodium`, 6 | exposed to Javascript by the `sodium-native` module. This suite of primitives 7 | has a strong focus on being friendly, hard to misuse and "high level". Many 8 | of the primitives we will be looking at here also have 9 | equivalents in the core `crypto` module, which is based on OpenSSL. 10 | 11 | ## Side quest 12 | 13 | `sodium-native` is a native module, which in itself can be a side quest to 14 | install. However we (@mafintosh and @emilbayes) try hard to provide prebuilt 15 | binaries for all major platforms and releases of node and electron. This means 16 | you shouldn't have to compile anything when installing the module. 17 | 18 | ```sh 19 | npm install sodium-native 20 | ``` 21 | 22 | ## Buffers 23 | 24 | One of the fundamental building blocks that make cryptography practical and 25 | safe (and fun!) in Javascript is the core `Buffer` prototype. `Buffer`s today 26 | are the same as `Uint8Arrays` with some extra methods to make working with them 27 | easier. `Buffer`s in node is the closest you get to raw memory access, but in a 28 | safe manner, so you don't have overflow bugs like you do in C/C++. Below are some of 29 | the most important `Buffer` methods you will be using in the workshop. Namely, 30 | allocating a specific number of bytes, checking for equality and 31 | converting the Buffer back to something that is a printable string: 32 | 33 | ```js 34 | // Creating buffers 35 | Buffer.alloc(32) // Allocate empty 32 byte Buffer 36 | Buffer.from('Hello, World!') // Allocate buffer and write 'Hello world' 37 | Buffer.from('48656c6c6f20776f726c64', 'hex') // Decode string from `hex` 38 | Buffer.alloc(32).fill('Hello') // Allocate 32 byte Buffer and repeat 'Hello' 39 | 40 | buf1.equals(buf2) // Check whether buf1 and buf2 are equal byte by byte 41 | 42 | // Converting to printable strings 43 | buf.toString('hex') // Octets in as hexadecimal 44 | buf.toString('base64') // Octets as ascii safe string (base64) 45 | ``` 46 | 47 | Being intimate with `Buffer`s is key to working efficiently with crypto. 48 | 49 | ## Another side quest 50 | 51 | Spin up a node REPL and play around with the Buffer commands listed 52 | above. For example, try generating a 13 byte `Buffer` and write `Hello, World!` 53 | to it. Also try with a 32 byte `Buffer` and see the difference. Decoding 54 | different encodings such as `hex` and `base64` is also very important for later 55 | exercises. 56 | 57 | ## Hashing 58 | 59 | Hash functions are a class of mathematical functions that transform an arbitrary 60 | sequence of data into a fixed size digest. The digest is also often called a 61 | hash or a fingerprint, because the idea is that a hash function should produce 62 | a unique result for every unique piece of data. There are many varieties of 63 | hash functions but here we will only work with cryptographically secure hash 64 | functions. 65 | 66 | Cryptographic hash functions provide a much strong guarantee regarding the 67 | uniqueness of the fingerprint, but at the cost of requiring more computation, 68 | and hence, time. However for the particular hash function used by `libsodium`, 69 | context switching from V8 to C will become a bottleneck before hashing does :) 70 | 71 | In `libsodium` the general purpose hash function is called BLAKE2b, and is 72 | exposed as [`crypto_generichash(outputBuf, inputBuf, [key])`](https://github.com/sodium-friends/sodium-native#generic-hashing). 73 | 74 | ## Problem 75 | 76 | Write a new program, `hash-example.js`. Using `sodium-native` hash the 77 | string `"Hello, World!"` using the `sodium.crypto_generichash` primitive and 78 | print out the result as a `hex` string. Have a look at the constants defined in 79 | the `sodium-native` documentation (especially the `crypto_generichash_BYTES`), 80 | as these are important guidelines for working effectively with `libsodium`. 81 | 82 | # Testing 83 | 84 | Running your program it should produce the output 85 | `511bc81dde11180838c562c82bb35f3223f46061ebde4a955c27b3f489cf1e03`. 86 | 87 | [Continue to problem 07](07.md) 88 | -------------------------------------------------------------------------------- /problems/07.md: -------------------------------------------------------------------------------- 1 | # 07. Ensuring the integrity of the transaction log 2 | 3 |
4 | Solution and commentary to problem 06 5 | 6 | ```js 7 | var sodium = require('sodium-native') 8 | // Allocate Buffer for output hash 9 | var output = Buffer.alloc(sodium.crypto_generichash_BYTES) 10 | // Convert input to string and then Buffer 11 | var input = Buffer.from("Hello, World!") 12 | 13 | // Compute blake2b hash 14 | sodium.crypto_generichash(output, input) 15 | 16 | // Convert bytes to printable string 17 | console.log(output.toString('hex')) 18 | ``` 19 | 20 | You might find this way of calling functions a bit foreign, since there are so 21 | many manual steps, like allocating the output `Buffer` and converting a 22 | Javascript object to a string, and then converting it to a `Buffer`. But all for 23 | a reason! As mentioned in the introduction, this is because it gives you full 24 | control of the memory used, so if you are handling sensitive data, you can 25 | delete it as soon as you're done touching it (using `.fill(0)`) or you can 26 | reuse `Buffer`s in high performance scenarios. Also note that the data written 27 | to the output `Buffer` may contain any byte, and not all bytes are ascii 28 | friendly which is why you need to convert the `Buffer` to either `hex` or 29 | `base64` to print it. 30 | 31 |
32 | 33 | Using a hash we can start making our transaction log much more secure to 34 | mitigate the risk outlined in our threat model, in our earlier exercise. 35 | Remember the goal is to make sure that it is hard for an adversary to tamper 36 | with the transaction log. 37 | 38 | A first step in doing this is adding integrity checks to the log. We can use the 39 | hashing primitive we learned about in the previous exercise to hash each entry 40 | of the log to make it easier to detect when tampering happens. An easy way to 41 | hash a log entry is to use `JSON.stringify` and adapt your solution from 06. 42 | 43 | ```js 44 | function appendToTransactionLog (entry) { 45 | // entry is the messages received by the bank. We wrap it in an extra object 46 | // as this makes verifying the hashes a lot easier 47 | log.push({ 48 | value: entry, 49 | hash: hashToHex(JSON.stringify(entry)) 50 | }) 51 | } 52 | ``` 53 | 54 | Hashing each item gives us the possibility of detecting any tampering with 55 | that particular entry. However as you may have guessed we only mitigate editing 56 | a particular record, while the most insidious attack is adding, deleting and 57 | reordering records. We could get around this by hashing the transaction log as 58 | a whole, every time we append to it, however this will become inefficient as the 59 | log grows and also seems plain wasteful. It also has some subtle security issues 60 | out of scope for now. 61 | 62 | A solution to all the above is called a hash chain. A hash chain is a list where 63 | each entry's hash value is the hash of the previous item's `hash` AND the 64 | current `value`. This makes the entries "causally dependent" (not casually 65 | dependent), and guarantees that you cannot reorder the log and it is easy to 66 | verify. 67 | 68 | ```js 69 | // One edge-case with referring to the previous hash is that you need a 70 | // "genesis" hash for the first entry in the log 71 | var genesisHash = Buffer.alloc(32).toString('hex') 72 | 73 | function appendToTransactionLog (entry) { 74 | var prevHash = log.length ? log[log.length - 1].hash : genesisHash 75 | log.push({ 76 | value: entry, 77 | hash: hashToHex(prevHash + JSON.stringify(entry)) 78 | }) 79 | } 80 | ``` 81 | 82 | ## Problem 83 | 84 | Convert the transaction log into a hash chain and verify the integrity when you 85 | start the bank. A good way to approach is to feed all the `entry.value`s to your 86 | reduce function and checking that you get the same `hash` as the persisted JSON 87 | file. 88 | 89 | ## Testing 90 | 91 | Test that the various `teller.js` commands still work as expected. Then try 92 | modifying the transaction log by hand and restart `bank.js`. Try all the attack 93 | scenarios (editing, adding, deleting and reordering). When restarted the bank 94 | should detect that the transaction log has been tampered with. 95 | 96 | [Continue to problem 08](08.md) 97 | -------------------------------------------------------------------------------- /problems/08.md: -------------------------------------------------------------------------------- 1 | # 08. Unforgeable fingerprints 2 | 3 |
4 | Solution to problem 07 5 | 6 | ```js 7 | // bank.js 8 | 9 | ``` 10 | 11 |
12 | 13 | Phew, now we are just getting started trying to grok basic cryptography. 14 | In the previous exercise we made sure that our banking application can verify 15 | if there has been any corruption of it's transaction log using hashing. 16 | 17 | However for our previous scheme to be secure it would require some operational 18 | measures that may not be desirable. If the latest hash is distributed securely 19 | to competing parties (customers, staff, directors) and they have the opportunity 20 | to verify it at their discretion, this scheme is actually pretty safe. This is 21 | the same idea behind distributing SHASUM file for binary releases, like how 22 | Node.js does it. You download the SHASUM file from an official source, here 23 | nodejs.org, and then you can download the binaries from any source and verify 24 | that the SHA sums match. 25 | 26 | In practise, there are still some major flaws present, since it is too hard to 27 | enforce the above scheme for a bank where the hash changes all the time. 28 | A very practical attack vector is for an adversary to simply modify some values 29 | in the transaction log and then re-generate another valid hash chain. Then the 30 | bank will never know it has been attacked! Let's try to fix this. 31 | 32 | Basically we need a way for the bank to verify that it was the one that wrote 33 | the hashes of the transaction log in the first place. That way an attacker 34 | couldn't simply rewrite the transaction log and generate another hash-chain. 35 | This property is called non-repudiation in crypto-lingo. 36 | 37 | ## Digital signatures and Key-pairs 38 | 39 | A cryptographic technique for achieving non-repudiation is digital signatures. 40 | A digital signature is a cryptographic primitive that has two operations, `sign` 41 | and `verify`, and works just like a real-world signature, but without any 42 | practical uncertainty. 43 | 44 | To be able to create signatures you need what is called a key-pair. If you have 45 | an account on Github you probably already have a SSH key pair, which is the same 46 | concept but based on some very different mathematics. A key-pair consists of a 47 | public key and a secret key. These are also sometimes known as the identity and 48 | private key. The idea is that you distribute your public key to everyone, and 49 | they use this key to verify any signature that you've made. The secret key is 50 | never to be revealed as this is like a pen used to make signatures. If anyone 51 | steals your secret key, they can impersonate you and it's game over. 52 | 53 | ## Signing with `libsodium` and `sodium-native` 54 | 55 | `sodium-native` exposes digital signatures under the `crypto_sign_*` namespace. 56 | The most important ones are: 57 | 58 | * `sodium.crypto_sign_keypair(publicKey, secretKey)` 59 | Generates a key pair, where `publicKey` and `secretKey` are the output 60 | `Buffer`s. They need to be exactly `sodium.crypto_sign_PUBLICKEYBYTES` and 61 | `sodium.crypto_sign_SECRETKEYBYTES` long. 62 | * `sodium.crypto_sign_detached(signature, message, secretKey)` 63 | The detached API writes the digital signature to `signature` `Buffer`, for 64 | `message` using `secretKey`. The signature `Buffer` needs to be 65 | `crypto_sign_BYTES` long. Like with hashing, this signature may contain 66 | non-printable bytes, so it is a good idea to convert to `hex` or `base64` if 67 | you want to work with it outside `Buffer`s. 68 | * `var bool = sodium.crypto_sign_verify_detached(signature, message, publicKey)` 69 | To verify a signature, you pass in `signature` `Buffer`, the original 70 | `message` `Buffer` and the corresponding `publicKey`. It will return `bool` 71 | whether the signature could be verified or not. 72 | 73 | This primitive will allow us to upgrade our bank's transaction log with an 74 | unforgeable fingerprint (or signature) so we can verify that only the bank is 75 | updating it. This will prevent anyone without access to the private key from 76 | modifying the transaction log in any way. 77 | 78 | Let's first do a basic exercise to get a bit more familiar with signing. 79 | 80 | ## Problem 81 | 82 | Use the `crypto_sign` APIs to make two new programs `sign.js` and `verify.js`. 83 | `sign.js` should generate a key-pair, sign a message from the command line and 84 | print out the message, public key and signature. `verify.js` should accept the 85 | public key, message and signature and verify that it is correct. 86 | 87 | ## Testing 88 | 89 | Try generating a few messages and keys and verify them. Then try tampering with 90 | the signature and messages and verify that the verify script rejects them. 91 | 92 | For extra credit try sending your signed messages to other people in the 93 | workshop and have them verify it. You may also think about the security around 94 | how to establish trust in a public key, ie. how do you know the public key 95 | belongs to the person you think owns it. 96 | 97 | [Continue to problem 09](09.md) 98 | -------------------------------------------------------------------------------- /problems/09.md: -------------------------------------------------------------------------------- 1 | # 09. Signing the transaction log 2 | 3 |
4 | Solution to problem 08 5 | 6 | ```js 7 | // sign.js 8 | var sodium = require('sodium-native') 9 | 10 | var publicKey = Buffer.alloc(sodium.crypto_sign_PUBLICKEYBYTES) 11 | var secretKey = Buffer.alloc(sodium.crypto_sign_SECRETKEYBYTES) 12 | sodium.crypto_sign_keypair(publicKey, secretKey) 13 | 14 | var message = Buffer.from('Hello world!') 15 | var signature = Buffer.alloc(sodium.crypto_sign_BYTES) 16 | 17 | sodium.crypto_sign_detached(signature, message, secretKey) 18 | 19 | console.log('Public key: ' + publicKey.toString('hex')) 20 | console.log('Message: ' + message.toString()) 21 | console.log('Signature: ' + signature.toString('hex')) 22 | ``` 23 | 24 | ```js 25 | // verify.js 26 | 27 | ``` 28 | 29 |
30 | 31 | Now that we have a basic understanding on how digital signatures work we can 32 | start securing the transaction log with this new primitive. 33 | 34 | Remember, since we already have a hash chain of all transactions the only thing 35 | missing is making sure we, the bank, are the only ones that are generating the 36 | hash chain. 37 | 38 | ## Problem 39 | 40 | Using the signature APIs you learned about in the previous exercise, extend the 41 | bank to: 42 | 43 | 1. Check if a existing key-pair is stored on disk, if so load it. 44 | 2. If not, generate a new key-pair and store it. 45 | 3. When you generate a new hash for a transaction, sign _the hash_ using the secret key 46 | and store the signature as a new property `signature`, next to the `hash` and 47 | `value` properties. 48 | 4. When loading the transaction log, extend verification to validate the 49 | signatures of the hashes in addition to the hashes themselves. 50 | 51 | You might be thinking that storing the key-pair right next to the transaction 52 | log is not very safe. This is much more of an operational problem (which 53 | also has technical mitigations) but for the purpose of our workshop it will 54 | suffice. In a real bank, you might use a Hardware Secure Module (HSM), which is a 55 | logical separate computing unit in the server, from which the secret key never 56 | leaves. [AWS has a CloudHSM Classic](https://aws.amazon.com/cloudhsm/pricing-classic/) 57 | product which gives you a dedicated machine with it's own HSM. However, the 58 | upfront cost is $5,000 USD and there is a $1,500 USD monthly maintenance cost. 59 | 60 | ## Testing 61 | 62 | Make sure your bank works the same way as before. Then stop the bank and try 63 | tampering with the log. The bank should reject the bad transaction log, even if 64 | the hashes are correct. 65 | 66 | [Continue to problem 10](10.md) 67 | -------------------------------------------------------------------------------- /problems/10.md: -------------------------------------------------------------------------------- 1 | # 10. Updating the threat model 2 | 3 | By now our bank has mitigated the original threat model to a degree where the 4 | threat has shifted from the transaction log itself, and unto the key-pair. This 5 | means that we have centralised security onto something that is easier to reason 6 | about and has a much smaller attack surface, albeit being even more sensitive 7 | to the operation of our bank. However, this problem is now more a question of 8 | operations and policy than cryptography. 9 | 10 | This also means that our priorities shift as we now have other threats that 11 | pose higher risk. In this age of data leaks, we want to make sure that if an 12 | adversary, eg. a three letter gov't agency, breaks into our bank server at night 13 | and steals the transaction log, they stand to learn very little about the banks 14 | business. 15 | 16 | To achieve this, we need to introduce a new cryptographic primitive, symmetric 17 | encryption. 18 | 19 | Symmetric crypto dates all the way back to at least Julius Caesar, who used it 20 | to communicate securely with his generals. Much has happened since then, but the 21 | basic idea is the same, you have a key that is used for both the `encrypt` and 22 | `decrypt` operations. In modern schemes you often also need a `nonce` which is 23 | often a random piece of data, that is not required to be secret, but protects 24 | against a several classes of attacks. 25 | 26 | Using `sodium-native` this functionality is exposed through the 27 | `crypto_secretbox` APIs: 28 | 29 | * `sodium.crypto_secretbox_easy(cipher, message, nonce, secretKey)` 30 | Encrypt `message` `Buffer` into `cipher` `Buffer` with `nonce` and 31 | `secretKey`. The secret key must be `sodium.crypto_secretbox_KEYBYTES` and is 32 | best generated using the `sodium.randombytes_buf` API. This key must be 33 | persisted somehow. `nonce` should be another random buffer of size 34 | `sodium.crypto_secretbox_NONCEBYTES`. The `cipher` `Buffer` should be 35 | `message.length + sodium.crypto_secretbox_MACBYTES` long. It is important that you 36 | never re-use a nonce to encrypt more than a single message. 37 | * `var bool = sodium.crypto_secretbox_open_easy(message, cipher, nonce, secretKey)` 38 | Decrypt `cipher` `Buffer` into `message` `Buffer` using `nonce` and `secretKey`. 39 | Will return a `boolean` depending on whether the cipher text could be decrypted. 40 | 41 | ## Problem 42 | 43 | Use the APIs described above to make three new programs, `secret-key.js` 44 | `encrypt.js` and `decrypt.js`. 45 | 46 | * `secret-key.js` should generate a secret key using the `randombytes_buf` api of 47 | the correct length 48 | * `encrypt.js` should accept a secret key and a message and print out the 49 | encrypted message and the random nonce used to encrypt it. 50 | * `decrypt.js` should accept the encrypted message, secret key and nonce and 51 | print out the plaintext message if valid. 52 | 53 | ## Testing 54 | 55 | Try running a couple of test messages like `Hello, World` through your encrypter 56 | and try decrypting them to see that it works. Then try tampering with some of 57 | the encrypted messages to see that decryption fails. 58 | 59 | [Continue to problem 11](11.md) 60 | -------------------------------------------------------------------------------- /problems/11.md: -------------------------------------------------------------------------------- 1 | # 11. Encrypting the transaction log 2 | 3 | Now that we know about encryption we are ready to make our bank's transaction log safe for data leaks. 4 | 5 | ## Problem 6 | 7 | What we want to do is similar to when we added digital signatures. Generate a new encryption key if one hasn't already been generated. Then every time we save the transaction log to disk encrypt it using the `secret_box` primitive. Remember to generate a new nonce every time you encrypt it and that you don't have to keep the nonce secret, it is perfectly safe to save this together with the ciphertext. 8 | 9 | Then when the bank is starting up, read the transaction log from disk and decrypt it. 10 | 11 | ## Solution 12 | 13 | Again, test that the bank works similar to before but also check that the transaction log on disk is encrypted (looks garbled :)). Try modifying the ciphertext and see that the bank rejects it. 14 | 15 | [Continue to problem 12](12.md) 16 | -------------------------------------------------------------------------------- /problems/12.md: -------------------------------------------------------------------------------- 1 | # 12. Supporting multiple bank customers. 2 | 3 | Congrats on making it this far! We now have a bank, a threat model and a bunch of crypto that secures it. 4 | Our bank needs more functionality in order to be useful and not just secure. 5 | 6 | One of those features is supporting multiple customers per bank instead of just a single one. 7 | 8 | ## Problem 9 | 10 | Make a new command `{cmd: 'register', customerId: someId}` that creates a new customer in the bank. 11 | 12 | The customer should send the customerId on every `withdraw`/`deposit`/`balance` operation it does with the bank. 13 | and the bank should add the customer id on every entry in its transaction log to support multiple customers. 14 | 15 | Expand the `teller.js` program to require a customerId and to support to `register` command 16 | 17 | ## Solution 18 | 19 | Spin up `bank.js` and use `teller.js` to register some customers and deposit/withdraw some money. 20 | The two customers operations should not collide, but the bank should still only have one transaction log. 21 | 22 | [Continue to problem 13](13.md) 23 | -------------------------------------------------------------------------------- /problems/13.md: -------------------------------------------------------------------------------- 1 | # 13. Securing the customers 2 | 3 | So now we support multiple customers, but it isn't really secure. 4 | If customers can guess other customer's ids, they can easily pretend to be them and steal all their money! 5 | 6 | That is no fun :( 7 | 8 | Let's update our threat model to include customers not being able to pose as other customers. 9 | 10 | The way we can do this is make each customer generate a signing key pair when registering with the bank and then saving the secret key and using the public key as their customer id. 11 | 12 | ## Problem 13 | 14 | Change the `teller.js` and `bank.js` to use a key pair as their identity. Have the teller sign every command object it sends to the bank similar to how the bank signs the transaction log. 15 | 16 | Then have the `bank.js` verify that a command has been signed by the customer before applying it to its transaction log. 17 | 18 | ## Solution 19 | 20 | Again, test that commands like `withdraw`/`deposit`/`balance` still work. 21 | Then try impersonating another customer without having their secret key and see if the bank rejects it. 22 | 23 | [Continue to problem 14](14.md) 24 | -------------------------------------------------------------------------------- /problems/14.md: -------------------------------------------------------------------------------- 1 | # 14. Fixing replay attacks 2 | 3 | This is an advanced exercise. If you're not up for it you can skip this one, although we promise it'll be lots of fun. 4 | 5 | There is a security issue with the solution from the previous exercise. Basically, an attacker can intercept a message to the bank (like a `withdraw` command) and send it to the bank more than once. This is called "replaying" a command. Our current bank will accept the same command over and over again, each time thinking it is valid. 6 | 7 | Again we can re-use the hash-chain data structure from a previous exercise to solve this. 8 | 9 | ## Problem 10 | 11 | Expand the `bank.js` program to return the latest hash of the item in the transaction log the customer last appended, every time the customer does an operation (except for register of course since that would always be the first operation). 12 | 13 | Have the `teller.js` store the latest received hash and include this hash as another value in the message that is signed and sent by the teller. That way `bank.js` can verify the order of a message received from `teller.js` is correct and not being replayed. A replayed message will include the hash from 2 or more transactions ago, not the last transaction. 14 | 15 | ## Testing 16 | 17 | Try recording a `deposit` message from the teller and send it multiple times to the bank. The bank should reject any replays. 18 | 19 | [Continue to problem 15](15.md) 20 | -------------------------------------------------------------------------------- /problems/15.md: -------------------------------------------------------------------------------- 1 | # 15. Secure memory. 2 | 3 | This is the last official exercise of the workshop. But, that doesn't mean your journey ends here. There are many things we can do to further secure our bank, but let's get to that later. 4 | 5 | You may have noticed that `bank.js` is dealing with very sensitive data. The secret keys are especially important to keep secure. Many people often forget that application memory can be an attack vector as well. Memory might be swapped to disk in plain text or an attacker might force an application to do a core dump and to get access to the memory. 6 | 7 | Luckily libsodium has primitives that can help us lock down our application memory *completely*. 8 | 9 | Take a look at the `sodium.sodium_malloc(size)` api exposed in sodium-native. This API allows you to make a node buffer that gives you full control over when memory can be accessed or written to. It also makes sure secure memory is never swapped to disk (this is actually protected by your OS kernel!). 10 | 11 | Using the `sodium.sodium_mprotect_noaccess(buffer)` api you can mark a secure buffer as unaccessible. This means that if anyone is trying to read this memory (your own program included) it will *immediately* crash. You can then use `sodium.sodium_mprotect_readwrite(buf)` to make it readable/writable again. This is a very powerful construct as it gives you full control over sensitive memory in your application. 12 | 13 | ## Problem 14 | 15 | Use secure buffers for all secret keys used by `bank.js`, and make sure to mark them as `noaccess` if they aren't being used to encrypt/sign something 16 | 17 | ## Solution 18 | 19 | Again test that your application still works and try making your application crash by trying to read from a buffer that is marked with `noaccess`. 20 | 21 | [Continue to extra-credit](extra-credit.md) 22 | -------------------------------------------------------------------------------- /problems/README.md: -------------------------------------------------------------------------------- 1 | # 00. Welcome 2 | 3 | In this workshop you will learn about cryptographic engineering without too much 4 | mathematics. We will be learning the cryptographic library called `libsodium`, 5 | which is in contrast to the `crypto` module in Node.js core, which is based on 6 | `openssl`. `libsodium` is a collection of high-level cryptographic primitives 7 | which are misuse-resistant, meaning it is hard to get wrong. 8 | 9 | For this workshop we will be doing all our programming in Javascript on Node.js, 10 | but since `libsodium` is written in C, we will be using the bindings called 11 | `sodium-native`. The bindings contain prebuilts for most common platforms, so 12 | you should not have to worry about compiling and tooling. You will also see that 13 | the bindings are a very thin wrapper, so the API might seem a bit foreign or 14 | very low-level, but this is a feature, as it gives you complete control over 15 | everything that happens inside `libsodium`. As you will learn through these 16 | exercises, having full control and complete introspection is vital to reason 17 | about the security properties of a system. Hopefully you will come to appreciate 18 | the simplicity of the API, despite the extra boilerplate that it causes. 19 | 20 | In this workshop we will be building a bank, which will iteratively show you 21 | how your bank is insecure and how we can improve that with cryptographic 22 | primitives. We will start by focusing on securing the bank vault itself, and 23 | then how each customer can secure their account against a malicious bank. 24 | 25 | [Let's get started! Problem 01](01.md) 26 | -------------------------------------------------------------------------------- /problems/extra-credit.md: -------------------------------------------------------------------------------- 1 | # Extra credit 2 | 3 | Whoa, so you finished the entire workshop! Good job. 4 | What you can do now is ask @mafintosh or @emilbayes about ways to secure your bank even more. 5 | 6 | For example 7 | 8 | * Make sure communication between the teller and bank is always encrypted to ensure privacy 9 | * Use key derivation so only have a single secret key that is used generate encryption and signing keys 10 | 11 | Good luck! 12 | -------------------------------------------------------------------------------- /solutions/01/bank.js: -------------------------------------------------------------------------------- 1 | var jsonStream = require('duplex-json-stream') 2 | var net = require('net') 3 | 4 | var server = net.createServer(function (socket) { 5 | socket = jsonStream(socket) 6 | 7 | socket.on('data', function (msg) { 8 | console.log('Bank received:', msg) 9 | socket.write({cmd: 'balance', balance: 0}) 10 | }) 11 | }) 12 | 13 | server.listen(3876) 14 | -------------------------------------------------------------------------------- /solutions/01/teller.js: -------------------------------------------------------------------------------- 1 | var jsonStream = require('duplex-json-stream') 2 | var net = require('net') 3 | 4 | var client = jsonStream(net.connect(3876)) 5 | 6 | client.on('data', function (msg) { 7 | console.log('Teller received:', msg) 8 | }) 9 | 10 | client.end({cmd: 'balance'}) 11 | -------------------------------------------------------------------------------- /solutions/02/bank.js: -------------------------------------------------------------------------------- 1 | var jsonStream = require('duplex-json-stream') 2 | var net = require('net') 3 | 4 | var log = [] 5 | 6 | var server = net.createServer(function (socket) { 7 | socket = jsonStream(socket) 8 | 9 | socket.on('data', function (msg) { 10 | console.log('Bank received:', msg) 11 | 12 | switch (msg.cmd) { 13 | case 'balance': 14 | socket.end({cmd: 'balance', balance: log.reduce(reduceLog, 0)}) 15 | break 16 | case 'deposit': 17 | log.push(msg) 18 | socket.end({cmd: 'balance', balance: log.reduce(reduceLog, 0)}) 19 | break 20 | default: 21 | socket.end({cmd: 'error', msg: 'Unknown command'}) 22 | break 23 | } 24 | 25 | }) 26 | }) 27 | 28 | server.listen(3876) 29 | 30 | function reduceLog (balance, entry) { 31 | return balance + entry.amount 32 | } 33 | -------------------------------------------------------------------------------- /solutions/02/teller.js: -------------------------------------------------------------------------------- 1 | var jsonStream = require('duplex-json-stream') 2 | var net = require('net') 3 | 4 | var client = jsonStream(net.connect(3876)) 5 | var argv = process.argv.slice(2) 6 | 7 | var command = argv[0] 8 | 9 | client.on('data', function (msg) { 10 | console.log('Teller received:', msg) 11 | }) 12 | 13 | switch (command) { 14 | case 'deposit': 15 | var amount = parseFloat(argv[1]) 16 | client.end({cmd: 'deposit', amount: amount}) 17 | break 18 | 19 | case 'balance': 20 | client.end({cmd: 'balance'}) 21 | break 22 | 23 | case 'help': 24 | default: 25 | console.log('node teller.js [CMD]') 26 | } 27 | -------------------------------------------------------------------------------- /solutions/bank.js: -------------------------------------------------------------------------------- 1 | var jsonStream = require('duplex-json-stream') 2 | var net = require('net') 3 | var sodium = require('sodium-universal') 4 | 5 | var siginfo = require('siginfo') 6 | 7 | var server = net.createServer(function (socket) { 8 | socket = jsonStream(socket) 9 | socket.on('data', function (msg) { 10 | ondata(socket, msg) 11 | }) 12 | }) 13 | 14 | var log = [ 15 | {cmd: 'genesis', hash: Buffer.alloc(sodium.crypto_generichash_BYTES).toString('base64')} 16 | ] 17 | var state = { balance: 0 } 18 | 19 | function ondata(sock, msg) { 20 | switch (msg.cmd) { 21 | case 'deposit': 22 | var entry = { 23 | value: msg, 24 | hash: hashchain(log[log.length - 1], msg) 25 | } 26 | 27 | state.balance += entry.value.amount 28 | log.push(entry) 29 | sock.write({res: 'success', msg: 'Balance: ' + state.balance}) 30 | return 31 | 32 | case 'withdraw': 33 | if (state.balance - msg.amount < 0) { 34 | sock.write({res: 'error', msg: 'Insufficient balance'}) 35 | return 36 | } 37 | 38 | var entry = { 39 | value: msg, 40 | hash: hashchain(log[log.length - 1], msg) 41 | } 42 | 43 | state.balance -= entry.value.amount 44 | log.push(entry) 45 | sock.write({res: 'success', msg: 'Balance: ' + state.balance}) 46 | return 47 | 48 | default: 49 | sock.write({res: 'error', msg: 'Unknown command'}) 50 | } 51 | } 52 | 53 | function hashchain (prev, cur) { 54 | // Important: If two messages do not have a natrual seperation 55 | // it is very important to use a seperating token. Example being 56 | // concatting 'car' and 'pet', which without a seperator could 57 | // introduce a subtle security issue in using the token 'carpet' 58 | var inputBuf = Buffer.from(prev.hash + JSON.stringify(cur)) 59 | 60 | // Preallocate our hash result buffer 61 | var hashBuf = Buffer.alloc(sodium.crypto_generichash_BYTES) 62 | sodium.crypto_generichash(hashBuf, inputBuf) 63 | return hashBuf.toString('base64') 64 | } 65 | 66 | siginfo(function () { 67 | return { 68 | state: state, 69 | log: log 70 | } 71 | }) 72 | 73 | server.listen(3876) 74 | -------------------------------------------------------------------------------- /solutions/teller.js: -------------------------------------------------------------------------------- 1 | var jsonStream = require('duplex-json-stream') 2 | var net = require('net') 3 | 4 | var client = jsonStream(net.connect(3876)) 5 | 6 | var argv = process.argv.slice(2) 7 | 8 | var command = argv[0] 9 | 10 | switch (command) { 11 | case 'deposit': 12 | var amount = argv[1] 13 | send({cmd: 'deposit', amount: parseFloat(amount, 10)}) 14 | break 15 | 16 | case 'withdraw': 17 | var amount = argv[1] 18 | send({cmd: 'withdraw', amount: parseFloat(amount, 10)}) 19 | break 20 | 21 | case 'help': 22 | default: 23 | console.error(` 24 | teller [CMD] [ARGS...] 25 | 26 | deposit [AMOUNT] 27 | 28 | withdraw [AMOUNT] 29 | `.replace(/^ /gm, '').trim()) 30 | client.end() 31 | process.exit(1) 32 | } 33 | 34 | function send(obj) { 35 | client.once('data', function (data) { 36 | console.log(data) 37 | }) 38 | client.write(obj) 39 | client.end() 40 | } 41 | --------------------------------------------------------------------------------