diff --git a/.gitignore b/.gitignore index 1a4a6e9..027fc9b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ npm-debug.log mywebsite tls.key tls.crt -stats.html \ No newline at end of file +stats.html +/vendor diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..fc197ef --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,42 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/NYTimes/gziphandler" + packages = ["."] + revision = "2600fb119af974220d3916a5916d6e31176aac1b" + version = "v1.0.1" + +[[projects]] + name = "github.com/julienschmidt/httprouter" + packages = ["."] + revision = "348b672cd90d8190f8240323e372ecd1e66b59dc" + version = "v1.2.0" + +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = [ + "acme", + "acme/autocert" + ] + revision = "505ab145d0a99da450461ae2c1a9f6cd10d1f447" + +[[projects]] + branch = "v2" + name = "gopkg.in/mgo.v2" + packages = [ + ".", + "bson", + "internal/json", + "internal/sasl", + "internal/scram" + ] + revision = "9856a29383ce1c59f308dd1cf0363a79b5bef6b5" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "9162cd6548eca5cba13aa4966fdb690196fa0ad08b56779e18448b9d94c0985c" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..638cde8 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,46 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + name = "github.com/NYTimes/gziphandler" + version = "1.0.1" + +[[constraint]] + name = "github.com/julienschmidt/httprouter" + version = "1.2.0" + +[[constraint]] + branch = "master" + name = "golang.org/x/crypto" + +[[constraint]] + branch = "v2" + name = "gopkg.in/mgo.v2" + +[prune] + go-tests = true + unused-packages = true diff --git a/posts/Projects/2018-12-12-mgcrypt.md b/posts/Projects/2018-12-12-mgcrypt.md new file mode 100644 index 0000000..ce370a0 --- /dev/null +++ b/posts/Projects/2018-12-12-mgcrypt.md @@ -0,0 +1,185 @@ +# MGCrypt +A dead simple [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) file encryption tool I wrote. + +[Check out the source code here.](https://github.com/mgerb/mgcrypt) + +*** + +## Why? + +I've been wanting a simple tool to encrypt files on the fly. There are various tools +that already do this, but I thought this would be a perfect learning opportunity. + +## What language? + +Go! Of course I picked Go for this because it is perfect. + +- it's fast +- compiles cross platform executables +- has crypto libraries built in +- my favorite language :) + +## How does [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) work? + +To be honest I don't know the nitty gritty details of how this algorithm encrypts files, +but it has been thoroughly tested and is secure enough. AES is a symmetric encryption algorithm. +This means that the same key can be used to both encrypt and decrypt. This is perfect for +securing files. + +## Generating keys + +AES has a key size of 128, 192 or 256 bits. This means I would need to have a 16 character +long password just to reach the 128 bits. I first starting thinking how I would get around this. +Maybe I could just take the first 128 bits of the password, but what if the password wasn't +longer than 16 characters? Add more characters to it? + +This seems a little iffy if we are going to mess with passwords directly like this. Remember +that I just need 128 bits and they do not need to be characters. What if we hash the password +and take the first 128 bits of it? This is actually a potential solution and is known as a +truncated hash. + +### PBKDF2 + +I was thinking of using SHA256 to hash the password and use the truncated version bits. This +should work just fine, but SHA256 is a fairly fast hashing algorithm, which would open us +up to dictionary attacks. + +It turns out there is an algorithm designed for exactly what we need here; converting a password +to a 128 bit AES key. It's known as [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2). This algorithm +works perfectly because we can tune the cost it takes to generate a password. The algorithm also +allows us to supply a salt. + +### What's the point of the salt? + +The concept of a salt was a little confusing to me at first. I wondered why this arbitrary +data added to a password would increase security. A salt is just extra bits added to the +password to generate a unique AES key for each encrypted file. Keep in mind here we are +generating an AES key from a password. The key will then be used to encrypt/decrypt the file. + +Why would we store the salt in plain text on the file? Of course we could store it in a +secret database somewhere but this just isn't practical. The purpose of the salt is just +to make brute forcing a group of files much more difficult. + +Let's say we have a directory of files all encrypted with the same AES key. This means +that a hacker would only have to brute force one key and then they would have access to everything. +Using a salt along with the password to generate our AES key means that we produce a +different key for each individual file. This increases the time substantially that it will +take to crack every file. + +### Generating the salt + +Good thing PBKDF2 takes care of generating the AES key and all we need to provide is the +salt and the passphrase. We are going to use a 128 bit salt. This salt will then be stored on +the encrypted file. We can just take the first 128 bits of the file and append it to the +encrypted file. Then, we can just peel that salt off when we are decrypting! + +```Go +// generate AES key based on password input and salt from file +func generateKey(password, salt []byte) []byte { + return pbkdf2.Key(password, salt, 4096, 32, sha256.New) +} +``` + +There's a problem with this. This would mean that the first 128 bits of the encrypted file, +contain 128 bits of the unencrypted portion of the file. I easily solved this by grabbing +the first 128 bits of the SHA256 checksum of the original file and used that as a salt. + +```Go +// get the first 128 bits of the sha256 checksum of the file +func getEncryptionSalt(filedata []byte) []byte { + sum := sha256.Sum256(filedata) + return sum[:16] +} +``` + +## Encrypting + +The encrypt function should seem pretty straight forward. You may notice something called the IV, +or initialization vector. This is almost like the salt, but it doesn't have anything to do with +our passphrase or AES key. It just adds some extra randomness to the encrypted file. Like the salt, +these random bits are stored with the encrypted file. The AES tools included in Go provide this automatically. +The IV is actually stored at the beginning of the encrypted file after our salt. + +You can see at the return statement we are appending the salt to the beginning of the encrypted file. + +```Go +func encrypt(inputKey, data []byte) ([]byte, error) { + salt := getEncryptionSalt(data) + key := generateKey(inputKey, salt) + + // Empty array of 16 + data length + // Include the IV at the beginning + ciphertext := make([]byte, aes.BlockSize+len(data)) + + // Create the AES cipher + block, err := aes.NewCipher(key) + if err != nil { + return ciphertext, err + } + + // Slice of first 16 bytes + iv := ciphertext[:aes.BlockSize] + + // Write 16 rand bytes to fill iv + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return ciphertext, err + } + + // Return an encrypted stream + stream := cipher.NewCFBEncrypter(block, iv) + + // Encrypt bytes from plaintext to ciphertext + stream.XORKeyStream(ciphertext[aes.BlockSize:], data) + + // add the salt to the beginning of the ciphertext + return append(salt, ciphertext...), nil +} +``` + +## Decrypting + +The decrypt function looks very similar to the encrypt function, except we are peeling off +the salt right away. You can see the IV bits being removed as well before the file is +finally decrypted. + +```Go + +// get the first 128 bits from the file as the salt +func getDecriptionSalt(filedata []byte) []byte { + return filedata[:16] +} + +func decrypt(inputKey, data []byte) ([]byte, error) { + salt := getDecriptionSalt(data) + key := generateKey(inputKey, salt) + + // strip the salt from the file + data = data[16:] + + // Create the AES cipher + block, err := aes.NewCipher(key) + if err != nil { + return data, err + } + + // Before even testing the decryption, + // if the text is too small, then it is incorrect + if len(data) < aes.BlockSize { + return data, errors.New("Text is too short") + } + + // Get the 16 byte IV + iv := data[:aes.BlockSize] + + // Remove the IV from the ciphertext + data = data[aes.BlockSize:] + + // Return a decrypted stream + stream := cipher.NewCFBDecrypter(block, iv) + + // Decrypt bytes from ciphertext + stream.XORKeyStream(data, data) + + return data, nil +} +```