mirror of
https://github.com/mgerb/mywebsite
synced 2026-01-12 10:52:47 +00:00
new post - mgcrypt
This commit is contained in:
185
posts/Projects/2018-12-12-mgcrypt.md
Normal file
185
posts/Projects/2018-12-12-mgcrypt.md
Normal file
@@ -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
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user