Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor aead implementation #476

Merged
merged 20 commits into from
Aug 29, 2024
6 changes: 3 additions & 3 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[
{
"Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.",
"StartLine": 36,
"EndLine": 37,
"StartLine": 37,
"EndLine": 38,
"StartColumn": 3,
"EndColumn": 1,
"Match": "keyLen = chacha20poly1305.KeySize",
Expand All @@ -17,6 +17,6 @@
"Message": "",
"Tags": [],
"RuleID": "generic-api-key",
"Fingerprint": "cry/enc.go:generic-api-key:36"
"Fingerprint": "cry/enc.go:generic-api-key:37"
}
]
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
Most recent version is listed first.


# v0.1.10
- ong/cry: refactor aead implementation: https://github.com/komuw/ong/pull/476

# v0.1.9
- ong/middleware: logFunc should not be passed a http.ResponseWriter: https://github.com/komuw/ong/pull/474

Expand Down
36 changes: 25 additions & 11 deletions cry/enc.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
cryptoRand "crypto/rand"
"encoding/base64"
"errors"
"fmt"
"runtime"
"slices"

Expand Down Expand Up @@ -55,6 +56,19 @@ type Enc struct {
key []byte
}

// String implements [fmt.Stringer]
func (e Enc) String() string {
if len(e.key) <= 0 {
return "Enc{key:<EMPTY>}"
}
return fmt.Sprintf("Enc{key:%s<REDACTED>}", string(e.key[0]))
}

// GoString implements [fmt.GoStringer]
func (e Enc) GoString() string {
return e.String()
}

// New returns a [cipher.AEAD]
//
// It panics on error.
Expand All @@ -75,7 +89,7 @@ func New(secretKey string) Enc {
derivedKey := deriveKey(password, salt)

/*
Another option would be to use argon2.
Another option would be to use scrypt.
import "golang.org/x/crypto/scrypt"
key := scrypt.Key("password", salt, 32768, 8, 1, keyLen)
*/
Expand All @@ -98,17 +112,20 @@ func New(secretKey string) Enc {
func (e Enc) Encrypt(plainTextMsg string) (encryptedMsg []byte) {
msgToEncrypt := []byte(plainTextMsg)

// Select a random nonce, and leave capacity for the ciphertext.
// Select a random nonce.
// https://github.com/golang/crypto/blob/v0.26.0/chacha20poly1305/chacha20poly1305_test.go#L222
nonce := random(
e.aead.NonceSize(),
e.aead.NonceSize()+len(msgToEncrypt)+e.aead.Overhead(),
chacha20poly1305.NonceSizeX,
chacha20poly1305.NonceSizeX+len(msgToEncrypt)+chacha20poly1305.Overhead,
)

// Encrypt the message and append the ciphertext to the nonce.
encrypted := e.aead.Seal(nonce, nonce, msgToEncrypt, nil)

// Append the salt & nonce to encrypted msg.
// |salt|nonce|encryptedMsg|
encrypted = append(
// "you can send the nonce in the clear before each message; so long as it's unique." - agl
// "you can send the nonce in the clear before each message; so long as it's unique. it can even be a counter." - agl
// see: https://crypto.stackexchange.com/a/5818
//
// "salt does not need to be secret."
Expand All @@ -122,12 +139,12 @@ func (e Enc) Encrypt(plainTextMsg string) (encryptedMsg []byte) {

// Decrypt authenticates and un-encrypts the encryptedMsg using XChaCha20-Poly1305 and returns decrypted bytes.
func (e Enc) Decrypt(encryptedMsg []byte) (decryptedMsg []byte, err error) {
if len(encryptedMsg) < e.aead.NonceSize() {
if len(encryptedMsg) < chacha20poly1305.NonceSizeX {
return nil, errors.New("ong/cry: ciphertext too short")
}

// get salt
salt, encryptedMsg := encryptedMsg[:saltLen], encryptedMsg[saltLen:]
// get constituent parts
salt, nonce, ciphertext := encryptedMsg[:saltLen], encryptedMsg[saltLen:saltLen+chacha20poly1305.NonceSizeX], encryptedMsg[saltLen+chacha20poly1305.NonceSizeX:]

aead := e.aead
if !slices.Equal(salt, e.salt) {
Expand All @@ -141,9 +158,6 @@ func (e Enc) Decrypt(encryptedMsg []byte) (decryptedMsg []byte, err error) {
}
}

// Split nonce and ciphertext.
nonce, ciphertext := encryptedMsg[:aead.NonceSize()], encryptedMsg[aead.NonceSize():]

// Decrypt the message and check it wasn't tampered with.
return aead.Open(nil, nonce, ciphertext, nil)
}
Expand Down
41 changes: 41 additions & 0 deletions cry/enc_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cry

import (
"os"
"slices"
"strings"
"sync"
"testing"

Expand Down Expand Up @@ -108,6 +110,45 @@ func TestEnc(t *testing.T) {
attest.Equal(t, string(decryptedMsg), msgToEncrypt)
})

t.Run("encrypt/decrypt file", func(t *testing.T) {
t.Parallel()

msgToEncrypt := ""
var decryptedMsg []byte
key := tst.SecretKey()

dir := t.TempDir()
originalFile := dir + "/originalFile.txt"
encryptedFile := dir + "/encryptedFile.txt.encrypted"

{
err := os.WriteFile(originalFile, []byte(strings.Repeat("h", (50*1024*1024))), 0o666) // 50MB
attest.Ok(t, err)
}

{
b, err := os.ReadFile(originalFile)
attest.Ok(t, err)
msgToEncrypt = string(b)

enc := New(key)
er := os.WriteFile(encryptedFile, enc.Encrypt(msgToEncrypt), 0o666)
attest.Ok(t, er)
}

{
b, err := os.ReadFile(encryptedFile)
attest.Ok(t, err)

enc := New(key)
msg, err := enc.Decrypt(b)
attest.Ok(t, err)
decryptedMsg = msg
}

attest.Equal(t, string(decryptedMsg), msgToEncrypt)
})

t.Run("concurrency safe", func(t *testing.T) {
t.Parallel()

Expand Down
Loading