Skip to content
This repository was archived by the owner on Jun 12, 2024. It is now read-only.

Commit c0ecf92

Browse files
authored
Add Seal/Unseal and crypto token creation (#23)
Now it's possible to use Authenticated Encryption using MAC-then-Encrypt (MtE) approach. Also, now it's easier to create a state or CSRF token using `GenerateToken` and then verify it using `DecodeAndVerifyToken`.
1 parent fe47fe6 commit c0ecf92

File tree

4 files changed

+229
-11
lines changed

4 files changed

+229
-11
lines changed

pkg/crypto/encrypt.go

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package crypto
33
import (
44
"crypto/aes"
55
"crypto/cipher"
6+
"crypto/hmac"
67
"crypto/rand"
78
"crypto/sha512"
9+
"encoding/base64"
810
"encoding/hex"
911
"errors"
1012
"io"
@@ -14,31 +16,34 @@ var (
1416
// ErrCipherTooShort occurs when `Decrypt` does not
1517
// have input of enough length to decrypt using AES256
1618
ErrCipherTooShort = errors.New("crypto: cipher plainText is too short for AES encryption")
19+
// ErrCorruptedMessage occurs when an attempt of unsealing a message
20+
// does not pass the authentication check
21+
ErrCorruptedMessage = errors.New("crypto: the message didn't pass the authentication check")
1722
)
1823

1924
// PassphraseToKey converts a string to a key for encryption.
2025
//
2126
// This function must be used STRICTLY ONLY for generating
2227
// an encryption key out of a passphrase.
2328
// Please don't use this function for hashing user-provided values.
24-
// It uses SHA2 for simplicity but it's slower. User-provided data should use SHA3
25-
// because of its better performance.
29+
// It uses SHA2 for simplicity and it's faster but less secure than SHA3.
30+
// User-provided data should use SHA3 or bcrypt.
2631
func PassphraseToKey(passphrase string) (key []byte) {
2732
// SHA512/256 will return exactly 32 bytes which is exactly
2833
// the length of the key needed for AES256 encryption
2934
hash := sha512.Sum512_256([]byte(passphrase))
3035
return hash[:]
3136
}
3237

33-
// Encrypt encrypts content with a key using AES256
34-
func Encrypt(plainText, key []byte) (encrypted []byte, err error) {
38+
// Encrypt encrypts content with a key using AES256 CFB mode
39+
func Encrypt(plainText, key []byte) (cipherText []byte, err error) {
3540
// code is taken from here https://golang.org/pkg/crypto/cipher/#NewCFBEncrypter
3641
block, err := aes.NewCipher(key)
3742
if err != nil {
3843
return nil, err
3944
}
4045

41-
cipherText := make([]byte, aes.BlockSize+len(plainText))
46+
cipherText = make([]byte, aes.BlockSize+len(plainText))
4247
iv := cipherText[:aes.BlockSize]
4348
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
4449
return nil, err
@@ -60,8 +65,8 @@ func EncryptToString(plainText, key []byte) (string, error) {
6065
return hex.EncodeToString(bytes), nil
6166
}
6267

63-
// Decrypt decrypts content with a key using AES256
64-
func Decrypt(cipherText, key []byte) (decrypted []byte, err error) {
68+
// Decrypt decrypts content with a key using AES256 CFB mode
69+
func Decrypt(cipherText, key []byte) (plainText []byte, err error) {
6570
// code is taken from here https://golang.org/pkg/crypto/cipher/#NewCFBDecrypter
6671
block, err := aes.NewCipher(key)
6772
if err != nil {
@@ -75,17 +80,78 @@ func Decrypt(cipherText, key []byte) (decrypted []byte, err error) {
7580

7681
stream := cipher.NewCFBDecrypter(block, iv)
7782

78-
plainText := make([]byte, len(cipherText))
83+
plainText = make([]byte, len(cipherText))
7984
stream.XORKeyStream(plainText, cipherText)
8085

8186
return plainText, nil
8287
}
8388

8489
// DecryptFromString decrypts a string with a key
85-
func DecryptFromString(cipherTextStr string, key []byte) (decrypted []byte, err error) {
90+
func DecryptFromString(cipherTextStr string, key []byte) ([]byte, error) {
8691
cipherText, err := hex.DecodeString(cipherTextStr)
8792
if err != nil {
8893
return nil, err
8994
}
9095
return Decrypt(cipherText, key)
9196
}
97+
98+
// Seal implements authenticated encryption using the MAC-then-Encrypt (MtE) approach.
99+
// It's using SHA3-256 for MAC and AES256 CFB for encryption.
100+
// https://en.wikipedia.org/wiki/Authenticated_encryption#MAC-then-Encrypt_(MtE)
101+
func Seal(plainText, key []byte) (cipherText []byte, err error) {
102+
mac := hmac.New(sha512.New512_256, key)
103+
104+
// the doc says it never returns an error, but we don't trust it
105+
_, err = mac.Write(plainText)
106+
if err != nil {
107+
return nil, err
108+
}
109+
messageMAC := mac.Sum(nil)
110+
messageAndMAC := append(plainText, messageMAC...)
111+
112+
return Encrypt(messageAndMAC, key)
113+
}
114+
115+
// SealToString runs `Seal` and then encodes the result into base64.
116+
func SealToString(plainText, key []byte) (string, error) {
117+
bytes, err := Seal(plainText, key)
118+
if err != nil {
119+
return "", err
120+
}
121+
return base64.StdEncoding.EncodeToString(bytes), nil
122+
}
123+
124+
// Unseal decrypts and authenticates the data encrypted by Seal
125+
func Unseal(cipherText, key []byte) (plainText []byte, err error) {
126+
messageAndMAC, err := Decrypt(cipherText, key)
127+
if err != nil {
128+
return nil, err
129+
}
130+
131+
splitPoint := len(messageAndMAC) - sha512.Size256
132+
messageMAC := messageAndMAC[splitPoint:]
133+
plainText = messageAndMAC[:splitPoint]
134+
135+
mac := hmac.New(sha512.New512_256, key)
136+
// the doc says it never returns an error, but we don't trust it
137+
_, err = mac.Write(plainText)
138+
if err != nil {
139+
return nil, err
140+
}
141+
expectedMAC := mac.Sum(nil)
142+
143+
if !hmac.Equal(expectedMAC, messageMAC) {
144+
return nil, ErrCorruptedMessage
145+
}
146+
147+
return plainText, err
148+
}
149+
150+
// UnsealFromString decodes from Base64 and applies `Unseal`.
151+
func UnsealFromString(cipherTextStr string, key []byte) ([]byte, error) {
152+
cipherText, err := base64.StdEncoding.DecodeString(cipherTextStr)
153+
if err != nil {
154+
return nil, err
155+
}
156+
return Unseal(cipherText, key)
157+
}

pkg/crypto/encrypt_test.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func TestPassphraseToKey(t *testing.T) {
1717
}
1818
}
1919

20-
func Test_EncryptDecrypt(t *testing.T) {
20+
func TestEncryptDecrypt(t *testing.T) {
2121
key := PassphraseToKey("some very secure passphrase no hacker can hack")
2222
text := []byte("some very secret text to encrypt")
2323

@@ -64,3 +64,60 @@ func Test_EncryptDecrypt(t *testing.T) {
6464
}
6565
})
6666
}
67+
68+
func TestSealUnseal(t *testing.T) {
69+
key := PassphraseToKey("some very secure passphrase no hacker can hack")
70+
text := []byte("some very secret text to encrypt")
71+
72+
t.Run("seals/unseals bytes", func(t *testing.T) {
73+
cipherText, err := Seal(text, key)
74+
if err != nil {
75+
t.Fatal(err)
76+
}
77+
if bytes.Equal(text, cipherText) {
78+
t.Fatal("cipher text must differ the original text")
79+
}
80+
81+
plainText, err := Unseal(cipherText, key)
82+
if err != nil {
83+
t.Fatal(err)
84+
}
85+
if !bytes.Equal(text, plainText) {
86+
t.Fatal("decrypted text must match the original text")
87+
}
88+
})
89+
90+
t.Run("seals/unseals a string", func(t *testing.T) {
91+
cipherTextString, err := SealToString(text, key)
92+
if err != nil {
93+
t.Fatal(err)
94+
}
95+
if string(text) == cipherTextString {
96+
t.Fatal("cipher text must differ the original text")
97+
}
98+
99+
plainText, err := UnsealFromString(cipherTextString, key)
100+
if err != nil {
101+
t.Fatal(err)
102+
}
103+
if !bytes.Equal(text, plainText) {
104+
t.Fatal("decrypted text must match the original text")
105+
}
106+
})
107+
108+
t.Run("authenticates the message", func(t *testing.T) {
109+
cipherText, err := Seal(text, key)
110+
if err != nil {
111+
t.Fatal(err)
112+
}
113+
cipherText[1] = 'x'
114+
115+
plainText, err := Unseal(cipherText, key)
116+
if err != ErrCorruptedMessage {
117+
t.Fatal(err)
118+
}
119+
if len(plainText) != 0 {
120+
t.Fatal("decrypted text must be empty in case of error")
121+
}
122+
})
123+
}

pkg/crypto/generate.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
package crypto
22

33
import (
4+
"bytes"
45
"crypto/rand"
6+
"encoding/gob"
57
"encoding/hex"
8+
"errors"
69
"io"
10+
"time"
711
)
812

13+
var (
14+
// ErrTokenExpired occurs when the token lifetime is exceeded
15+
ErrTokenExpired = errors.New("crypto: token expired")
16+
)
17+
18+
type token struct {
19+
Data []byte
20+
CreatedAt time.Time
21+
}
22+
923
// GenerateRandomString generates a random string with a given length
1024
func GenerateRandomString(length int) (string, error) {
1125
size := length
@@ -20,3 +34,42 @@ func GenerateRandomString(length int) (string, error) {
2034
}
2135
return hex.EncodeToString(bytes)[0:length], nil
2236
}
37+
38+
// GenerateToken generates a sealed token with a given ID and timestamp for
39+
// future verification.
40+
func GenerateToken(data, key []byte) (tokenStr string, err error) {
41+
t := token{
42+
Data: data,
43+
CreatedAt: time.Now(),
44+
}
45+
b := bytes.NewBuffer(nil)
46+
enc := gob.NewEncoder(b)
47+
err = enc.Encode(t)
48+
if err != nil {
49+
return "", err
50+
}
51+
52+
return SealToString(b.Bytes(), key)
53+
}
54+
55+
// DecodeAndVerify unseals the token and verifies its lifetime
56+
func DecodeAndVerifyToken(tokenStr string, key []byte, lifetime time.Duration) (data []byte, err error) {
57+
plainText, err := UnsealFromString(tokenStr, key)
58+
if err != nil {
59+
return nil, err
60+
}
61+
b := bytes.NewBuffer(plainText)
62+
dec := gob.NewDecoder(b)
63+
64+
var t token
65+
err = dec.Decode(&t)
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
if t.CreatedAt.Add(lifetime).Before(time.Now()) {
71+
return nil, ErrTokenExpired
72+
}
73+
74+
return t.Data, nil
75+
}

pkg/crypto/generate_test.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package crypto
22

3-
import "testing"
3+
import (
4+
"bytes"
5+
"testing"
6+
"time"
7+
)
48

59
func TestRandomString(t *testing.T) {
610
t.Run("does not repeat the same value twice", func(t *testing.T) {
@@ -35,3 +39,41 @@ func TestRandomString(t *testing.T) {
3539
}
3640
})
3741
}
42+
43+
func TestGenerateDecodeAndVerifyToken(t *testing.T) {
44+
key := PassphraseToKey("some very secure passphrase no hacker can hack")
45+
text := []byte("some very secret text to encrypt")
46+
47+
t.Run("preserves the data", func(t *testing.T) {
48+
token, err := GenerateToken(text, key)
49+
if err != nil {
50+
t.Fatal(err)
51+
}
52+
53+
data, err := DecodeAndVerifyToken(token, key, time.Hour)
54+
if err != nil {
55+
t.Fatal(err)
56+
}
57+
58+
if !bytes.Equal(text, data) {
59+
t.Fatal("data does not match with the initial text")
60+
}
61+
})
62+
63+
t.Run("expires", func(t *testing.T) {
64+
token, err := GenerateToken(text, key)
65+
if err != nil {
66+
t.Fatal(err)
67+
}
68+
69+
<-time.After(2 * time.Second)
70+
71+
data, err := DecodeAndVerifyToken(token, key, time.Second)
72+
if err != ErrTokenExpired {
73+
t.Fatal(err)
74+
}
75+
if len(data) != 0 {
76+
t.Fatal("data should be empty")
77+
}
78+
})
79+
}

0 commit comments

Comments
 (0)