mirror of
https://github.com/mgerb/mywebsite
synced 2026-01-11 02:12:53 +00:00
186 lines
6.5 KiB
Markdown
186 lines
6.5 KiB
Markdown
# 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
|
|
}
|
|
```
|