Skip to content

Commit bed879f

Browse files
committed
[WIP] symmetric: Add symmetric crypto routines
1 parent 9d9f015 commit bed879f

File tree

8 files changed

+289
-0
lines changed

8 files changed

+289
-0
lines changed

symmetric/aes.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package symmetric
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"errors"
7+
)
8+
9+
var (
10+
ErrInvalidKeyLength = errors.New("invalid key length")
11+
ErrInvalidMode = errors.New("invalid mode")
12+
)
13+
14+
type AESMode uint8
15+
16+
const (
17+
GCM AESMode = iota
18+
)
19+
20+
type AESEncrypterDecrypter struct {
21+
keyID string
22+
keyBytes []byte
23+
mode AESMode
24+
}
25+
26+
// NewAESEncrypterDecrypterFromSSLibSymmetricKey creates an
27+
// AESEncrypterDecrypter from an SSLibSymmetricKey.
28+
func NewAESEncrypterDecrypterFromSSLibSymmetricKey(key *SSLibSymmetricKey, mode AESMode) (*AESEncrypterDecrypter, error) {
29+
switch mode {
30+
case GCM:
31+
break
32+
default:
33+
return nil, ErrInvalidMode
34+
}
35+
36+
return &AESEncrypterDecrypter{
37+
keyID: key.KeyID,
38+
keyBytes: key.KeyVal,
39+
mode: mode,
40+
}, nil
41+
}
42+
43+
func (ed *AESEncrypterDecrypter) Encrypt(data []byte) ([]byte, error) {
44+
block, err := aes.NewCipher(ed.keyBytes)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
var ciphertext []byte
50+
51+
switch ed.mode {
52+
case GCM:
53+
gcm, err := cipher.NewGCMWithRandomNonce(block)
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
ciphertext = gcm.Seal(nil, nil, data, nil)
59+
}
60+
return ciphertext, nil
61+
}
62+
63+
func (ed *AESEncrypterDecrypter) Decrypt(data []byte) ([]byte, error) {
64+
block, err := aes.NewCipher(ed.keyBytes)
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
var plaintext []byte
70+
switch ed.mode {
71+
case GCM:
72+
gcm, err := cipher.NewGCMWithRandomNonce(block)
73+
if err != nil {
74+
return nil, err
75+
}
76+
77+
plaintext, err = gcm.Open(nil, nil, data, nil)
78+
if err != nil {
79+
return nil, err
80+
}
81+
}
82+
return plaintext, nil
83+
}
84+
85+
func (ed *AESEncrypterDecrypter) KeyID() (string, error) {
86+
return ed.keyID, nil
87+
}
88+
89+
func validateAESKeySize(key []byte) (int, error) {
90+
switch len(key) {
91+
// AES-128, AES-192, AES-256
92+
case 16, 24, 32:
93+
return len(key) / 8, nil
94+
default:
95+
return 0, ErrInvalidKeyLength
96+
}
97+
}

symmetric/aes_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package symmetric
2+
3+
import (
4+
"encoding/hex"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/secure-systems-lab/go-securesystemslib/symmetric/testdata"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
var plaintext = []byte("reallyimportant")
14+
15+
func TestRoundtrip(t *testing.T) {
16+
key, err := hex.DecodeString(string(testdata.AESKey))
17+
require.Nil(t, err)
18+
19+
fmt.Println(key)
20+
21+
aesED := &AESEncrypterDecrypter{
22+
keyID: "super secret key",
23+
keyBytes: key,
24+
mode: GCM,
25+
}
26+
27+
ciphertext, err := aesED.Encrypt(plaintext)
28+
assert.Nil(t, err)
29+
30+
decryptedPlaintext, err := aesED.Decrypt(ciphertext)
31+
assert.Nil(t, err)
32+
33+
assert.Equal(t, plaintext, decryptedPlaintext)
34+
}
35+
36+
func TestRoundtripCorrupted(t *testing.T) {
37+
key, err := hex.DecodeString(string(testdata.AESKey))
38+
require.Nil(t, err)
39+
40+
aesED := &AESEncrypterDecrypter{
41+
keyID: "super secret key",
42+
keyBytes: key,
43+
mode: GCM,
44+
}
45+
46+
ciphertext, err := aesED.Encrypt(plaintext)
47+
assert.Nil(t, err)
48+
49+
ciphertext[0] = ^ciphertext[0]
50+
51+
_, err = aesED.Decrypt(ciphertext)
52+
assert.ErrorContains(t, err, "message authentication failed")
53+
}

symmetric/encrypterdecrypter.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package symmetric
2+
3+
// Encrypter is the interface for an abstract symmetric block encryption
4+
// algorithm. The Encrypter interface is used to encrypt arbitrary byte
5+
// payloads, and returns the ciphertext, or an error. It is
6+
// cipher-agnostic.
7+
type Encrypter interface {
8+
Encrypt([]byte) ([]byte, []byte, error)
9+
KeyID() (string, error)
10+
}
11+
12+
// Decrypter is the interface to decrypt ciphertext.
13+
type Decrypter interface {
14+
Decrypt([]byte, []byte) ([]byte, error)
15+
KeyID() (string, error)
16+
}
17+
18+
// EncrypterDecrypter is the combined interface of Encrypter and Decrypter.
19+
type EncrypterDecrypter interface {
20+
Encrypter
21+
Decrypter
22+
}

symmetric/symmetric.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package symmetric
2+
3+
import (
4+
"encoding/hex"
5+
"errors"
6+
)
7+
8+
var ErrUnknownKeyType = errors.New("unknown key type")
9+
10+
type SSLibSymmetricCipher uint8
11+
12+
const (
13+
AES SSLibSymmetricCipher = iota
14+
)
15+
16+
type SSLibSymmetricKey struct {
17+
KeyID string `json:"keyid"`
18+
Cipher SSLibSymmetricCipher `json:"scheme"`
19+
KeySize int `json:"keysize"`
20+
KeyVal []byte `json:"keyval"`
21+
}
22+
23+
// LoadSymmetricKey returns an SSLibSymmetricKey object when provided a byte
24+
// array and cipher. Currently, AES-128/192/256 are supported.
25+
func LoadSymmetricKey(keyBytes []byte, cipher SSLibSymmetricCipher) (*SSLibSymmetricKey, error) {
26+
var key *SSLibSymmetricKey
27+
28+
switch cipher {
29+
case AES:
30+
keyBytes, err := hex.DecodeString(string(keyBytes))
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
keySize, err := validateAESKeySize(keyBytes)
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
key = &SSLibSymmetricKey{
41+
KeyID: "",
42+
Cipher: cipher,
43+
KeySize: keySize,
44+
KeyVal: keyBytes,
45+
}
46+
default:
47+
return nil, ErrUnknownKeyType
48+
}
49+
50+
keyID, err := calculateKeyID(key)
51+
if err != nil {
52+
return nil, err
53+
}
54+
key.KeyID = keyID
55+
return key, nil
56+
}

symmetric/symmetric_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package symmetric
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/secure-systems-lab/go-securesystemslib/symmetric/testdata"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestLoadSymmetricKey(t *testing.T) {
12+
tests := map[string]struct {
13+
keyBytes []byte
14+
cipher SSLibSymmetricCipher
15+
expectedKeyID string
16+
}{
17+
"AES key": {
18+
keyBytes: testdata.AESKey,
19+
cipher: AES,
20+
expectedKeyID: "3365676914098a99b563b1d5b90822916e78d1109640bbdaf196208db3edf908",
21+
},
22+
}
23+
for name, test := range tests {
24+
t.Run(name, func(t *testing.T) {
25+
key, err := LoadSymmetricKey(test.keyBytes, test.cipher)
26+
assert.Nil(t, err, fmt.Sprintf("unexpected error in test '%s'", name))
27+
assert.Equal(t, test.expectedKeyID, key.KeyID)
28+
})
29+
}
30+
}

symmetric/testdata/aes-key

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
6368616e676520746869732070617373776f726420746f206120736563726574

symmetric/testdata/testdata.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package testdata
2+
3+
import (
4+
_ "embed"
5+
)
6+
7+
//go:embed aes-key
8+
var AESKey []byte

symmetric/utils.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package symmetric
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/hex"
6+
7+
"github.com/secure-systems-lab/go-securesystemslib/cjson"
8+
)
9+
10+
func calculateKeyID(k *SSLibSymmetricKey) (string, error) {
11+
key := map[string]any{
12+
"cipher": k.Cipher,
13+
"keysize": k.KeySize,
14+
"keyval": k.KeyVal,
15+
}
16+
canonical, err := cjson.EncodeCanonical(key)
17+
if err != nil {
18+
return "", err
19+
}
20+
digest := sha256.Sum256(canonical)
21+
return hex.EncodeToString(digest[:]), nil
22+
}

0 commit comments

Comments
 (0)