Skip to content

Commit 9cf063a

Browse files
encryption Add support for encrypting content using hybrid RSA/AES encryption (#562)
<!-- Copyright (C) 2020-2022 Arm Limited or its affiliates and Contributors. All rights reserved. SPDX-License-Identifier: Apache-2.0 --> ### Description <!-- Please add any detail or context that would be useful to a reviewer. --> This change provides a method of encrypting a payload using hybrid RSA AES encryption. The payload itself is encrypted with the AES key which is fast but symmetric meaning both parties require the key. Therefore the key is encrypted with asymmetric RSA encryption and the encrypted key is included in the payload itself. RSA is not efficient on larger payloads so this method allows for fast encryption whilst also being asymmetric. Information on hybrid RSA/AES encryption can be found here https://www.ijrar.org/papers/IJRAR23B1852.pdf ### Test Coverage <!-- Please put an `x` in the correct box e.g. `[x]` to indicate the testing coverage of this change. --> - [x] This change is covered by existing or additional automated tests. - [ ] Manual testing has been performed (and evidence provided) as automated testing was not feasible. - [ ] Additional tests are not required for this change (e.g. documentation update). --------- Co-authored-by: Adrien CABARBAYE <[email protected]>
1 parent d4987e9 commit 9cf063a

File tree

5 files changed

+420
-0
lines changed

5 files changed

+420
-0
lines changed

changes/20250211153712.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: `encryption` Add support for encrypting content using hybrid [RSA/AES encryption](https://www.ijrar.org/papers/IJRAR23B1852.pdf)

utils/encryption/aesrsa/encryption.go

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package aesrsa
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"crypto/rand"
7+
"crypto/rsa"
8+
"crypto/sha256"
9+
"crypto/x509"
10+
"encoding/pem"
11+
"fmt"
12+
13+
"github.com/ARM-software/golang-utils/utils/commonerrors"
14+
"github.com/ARM-software/golang-utils/utils/filesystem"
15+
)
16+
17+
// ParsePEMBlock will parse the first PEM block found within path
18+
func ParsePEMBlock(path string) (block *pem.Block, err error) {
19+
if path == "" {
20+
err = fmt.Errorf("%w: no certificate provided", commonerrors.ErrUndefined)
21+
return
22+
}
23+
24+
if !filesystem.Exists(path) {
25+
err = fmt.Errorf("%w: could not find certificate at '%v'", commonerrors.ErrNotFound, path)
26+
return
27+
}
28+
29+
certBytes, err := filesystem.ReadFile(path)
30+
if err != nil {
31+
err = fmt.Errorf("%w: failed to read certificate: %v", commonerrors.ErrUnexpected, err.Error())
32+
return
33+
}
34+
35+
block, _ = pem.Decode(certBytes)
36+
if block == nil {
37+
err = fmt.Errorf("%w: failed to decode PEM block from certificate", commonerrors.ErrUnexpected)
38+
return
39+
}
40+
41+
return
42+
}
43+
44+
// DecryptHybridAESRSAEncryptedPayloadFromPrivateKey takes a path to an RSA private key and uses it to decode the AES
45+
// key in a hybrid encoded payload. This AES key is then used to decode the actual payload contents. Information of
46+
// the use of hybrid AES RSA encryption can be found here https://www.ijrar.org/papers/IJRAR23B1852.pdf
47+
func DecryptHybridAESRSAEncryptedPayloadFromPrivateKey(privateKeyPath string, payload *HybridAESRSAEncryptedPayload) (decrypted []byte, err error) {
48+
block, err := ParsePEMBlock(privateKeyPath)
49+
if err != nil {
50+
err = fmt.Errorf("%w: could not parse PEM block from '%v': %v", commonerrors.ErrUnexpected, privateKeyPath, err.Error())
51+
return
52+
}
53+
54+
if block == nil {
55+
err = fmt.Errorf("%w: block was empty", commonerrors.ErrEmpty)
56+
return
57+
}
58+
59+
return DecryptHybridAESRSAEncryptedPayloadFromBytes(block.Bytes, payload)
60+
}
61+
62+
// DecryptHybridAESRSAEncryptedPayloadFromPrivateKeyPath takes a path to an RSA private key PEM file and uses it to
63+
// decode the AES key in a hybrid encoded payload. This AES key is then used to decode the actual payload contents.
64+
// Information of the use of hybrid AES RSA encryption can be found here https://www.ijrar.org/papers/IJRAR23B1852.pdf
65+
func DecryptHybridAESRSAEncryptedPayloadFromBytes(block []byte, payload *HybridAESRSAEncryptedPayload) (decrypted []byte, err error) {
66+
if payload == nil {
67+
err = fmt.Errorf("%w: payload must not be nil", commonerrors.ErrUndefined)
68+
return
69+
}
70+
71+
priv, err := x509.ParsePKCS1PrivateKey(block)
72+
if err != nil {
73+
err = fmt.Errorf("%w: could not parse private key %v", commonerrors.ErrUnexpected, err.Error())
74+
return
75+
}
76+
77+
ciphertext, encryptedKey, nonce, err := DecodeHybridAESRSAEncryptedPayload(payload)
78+
if err != nil {
79+
err = fmt.Errorf("%w: could not decode payload: %v", commonerrors.ErrUnexpected, err.Error())
80+
return
81+
}
82+
83+
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, encryptedKey, []byte{})
84+
if err != nil {
85+
err = fmt.Errorf("%w: could not decrypt private key %v", commonerrors.ErrUnexpected, err.Error())
86+
return
87+
}
88+
89+
blockCipher, err := aes.NewCipher(aesKey)
90+
if err != nil {
91+
err = fmt.Errorf("%w: could not create new cipher %v", commonerrors.ErrUnexpected, err.Error())
92+
return
93+
}
94+
95+
aesGCM, err := cipher.NewGCM(blockCipher)
96+
if err != nil {
97+
err = fmt.Errorf("%w: could not create new GCM %v", commonerrors.ErrUnexpected, err.Error())
98+
return
99+
}
100+
101+
decrypted, err = aesGCM.Open(nil, nonce, ciphertext, nil)
102+
if err != nil {
103+
err = fmt.Errorf("%w: could not open ciphertext %v", commonerrors.ErrUnexpected, err.Error())
104+
return
105+
}
106+
107+
return
108+
}
109+
110+
// EncryptHybridAESRSAEncryptedPayloadFromBytes takes an x509 certificate for key encypherment and uses it to
111+
// encode a payload using hybrid RSA AES encryption where an AES key is used to encrypt the content in payload and the
112+
// AES key is encrypted using RSA encryption. AES encryption is used to encode the payload itself as it is faster than
113+
// RSA for larger payloads. RSA is used to encrypt the relatively small AES key and allows asymmetric encryption
114+
// whilst also being fast. More information can be found at https://www.ijrar.org/papers/IJRAR23B1852.pdf
115+
func EncryptHybridAESRSAEncryptedPayloadFromBytes(block []byte, payload []byte) (encrypted *HybridAESRSAEncryptedPayload, err error) {
116+
cert, err := x509.ParseCertificate(block)
117+
if err != nil {
118+
err = fmt.Errorf("%w: failed parsing certificate: %v", commonerrors.ErrUnexpected, err.Error())
119+
return
120+
}
121+
122+
rsaPub, ok := cert.PublicKey.(*rsa.PublicKey)
123+
if !ok {
124+
err = fmt.Errorf("%w: public key in certificate is not RSA", commonerrors.ErrInvalid)
125+
return
126+
}
127+
128+
aesKey := make([]byte, 32)
129+
_, err = rand.Read(aesKey)
130+
if err != nil {
131+
err = fmt.Errorf("%w: failed generating AES key: %v", commonerrors.ErrUnexpected, err.Error())
132+
return
133+
}
134+
135+
blockCipher, err := aes.NewCipher(aesKey)
136+
if err != nil {
137+
err = fmt.Errorf("%w: failed to create cipher: %v", commonerrors.ErrUnexpected, err.Error())
138+
return
139+
}
140+
141+
aesGCM, err := cipher.NewGCM(blockCipher)
142+
if err != nil {
143+
err = fmt.Errorf("%w: failed creating GCM: %v", commonerrors.ErrUnexpected, err.Error())
144+
return
145+
}
146+
147+
nonce := make([]byte, aesGCM.NonceSize())
148+
_, err = rand.Read(nonce)
149+
if err != nil {
150+
err = fmt.Errorf("%w: failed to generate nonce: %v", commonerrors.ErrUnexpected, err.Error())
151+
return
152+
}
153+
154+
ciphertext := aesGCM.Seal(nil, nonce, payload, nil)
155+
encryptedAESKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaPub, aesKey, []byte{})
156+
if err != nil {
157+
err = fmt.Errorf("%w: could not complete RSA encryption: %v", commonerrors.ErrUnexpected, err.Error())
158+
return
159+
}
160+
161+
encrypted = EncodeHybridAESRSAEncryptedPayload(ciphertext, encryptedAESKey, nonce)
162+
return
163+
}
164+
165+
// EncryptHybridAESRSAEncryptedPayloadFromCertificate takes a path to a valid x509 certificate for key encypherment
166+
// and uses it to encode a payload using hybrid RSA AES encryption where an AES key is used to encrypt the content in
167+
// payload and the AES key is encrypted using RSA encryption. AES encryption is used to encode the payload itself as
168+
// it is faster than RSA for larger payloads. RSA is used to encrypt the relatively small AES key and allows asymmetric
169+
// encryption whilst also being fast. More information can be found at https://www.ijrar.org/papers/IJRAR23B1852.pdf
170+
func EncryptHybridAESRSAEncryptedPayloadFromCertificate(certPath string, payload []byte) (encrypted *HybridAESRSAEncryptedPayload, err error) {
171+
block, err := ParsePEMBlock(certPath)
172+
if err != nil {
173+
err = fmt.Errorf("%w: could not parse PEM block from '%v': %v", commonerrors.ErrUnexpected, certPath, err.Error())
174+
return
175+
}
176+
177+
if block == nil {
178+
err = fmt.Errorf("%w: block was empty", commonerrors.ErrEmpty)
179+
return
180+
}
181+
182+
return EncryptHybridAESRSAEncryptedPayloadFromBytes(block.Bytes, payload)
183+
}
+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package aesrsa
2+
3+
import (
4+
"path/filepath"
5+
"strings"
6+
"testing"
7+
8+
"github.com/go-faker/faker/v4"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/ARM-software/golang-utils/utils/commonerrors/errortest"
13+
"github.com/ARM-software/golang-utils/utils/encryption/aesrsa/testhelpers"
14+
"github.com/ARM-software/golang-utils/utils/filesystem"
15+
)
16+
17+
func TestEncryption(t *testing.T) {
18+
t.Run("Valid", func(t *testing.T) {
19+
testCertPath, testKeyPath := testhelpers.GenerateTestCerts(t)
20+
require.FileExists(t, testCertPath)
21+
require.FileExists(t, testKeyPath)
22+
23+
content := []byte(faker.Sentence())
24+
25+
encrypted, err := EncryptHybridAESRSAEncryptedPayloadFromCertificate(testCertPath, content)
26+
require.NoError(t, err)
27+
require.NotEmpty(t, encrypted)
28+
29+
decoded, err := DecryptHybridAESRSAEncryptedPayloadFromPrivateKey(testKeyPath, encrypted)
30+
assert.NoError(t, err)
31+
assert.Equal(t, content, decoded)
32+
})
33+
34+
t.Run("decrypt: invalid key (missing)", func(t *testing.T) {
35+
testCertPath, _ := testhelpers.GenerateTestCerts(t)
36+
require.FileExists(t, testCertPath)
37+
38+
testKeyPath := filepath.Join(strings.Split(faker.Sentence(), " ")...)
39+
40+
content := []byte(faker.Sentence())
41+
42+
encrypted, err := EncryptHybridAESRSAEncryptedPayloadFromCertificate(testCertPath, content)
43+
require.NoError(t, err)
44+
require.NotEmpty(t, encrypted)
45+
46+
decoded, err := DecryptHybridAESRSAEncryptedPayloadFromPrivateKey(testKeyPath, encrypted)
47+
errortest.AssertErrorDescription(t, err, "could not find certificate")
48+
assert.Empty(t, decoded)
49+
})
50+
51+
t.Run("decrypt: invalid key (wrong key)", func(t *testing.T) {
52+
testCertPath, _ := testhelpers.GenerateTestCerts(t)
53+
require.FileExists(t, testCertPath)
54+
55+
_, testKeyPath := testhelpers.GenerateTestCerts(t)
56+
require.FileExists(t, testKeyPath)
57+
58+
content := []byte(faker.Sentence())
59+
60+
encrypted, err := EncryptHybridAESRSAEncryptedPayloadFromCertificate(testCertPath, content)
61+
require.NoError(t, err)
62+
require.NotEmpty(t, encrypted)
63+
64+
decoded, err := DecryptHybridAESRSAEncryptedPayloadFromPrivateKey(testKeyPath, encrypted)
65+
errortest.AssertErrorDescription(t, err, "decryption error")
66+
assert.Empty(t, decoded)
67+
})
68+
69+
t.Run("decrypt: invalid key (invalid file)", func(t *testing.T) {
70+
testCertPath, _ := testhelpers.GenerateTestCerts(t)
71+
require.FileExists(t, testCertPath)
72+
73+
testKeyPath := filepath.Join(t.TempDir(), faker.Word())
74+
err := filesystem.WriteFile(testKeyPath, []byte(faker.Sentence()), 0644)
75+
require.NoError(t, err)
76+
require.FileExists(t, testKeyPath)
77+
78+
content := []byte(faker.Sentence())
79+
80+
encrypted, err := EncryptHybridAESRSAEncryptedPayloadFromCertificate(testCertPath, content)
81+
require.NoError(t, err)
82+
require.NotEmpty(t, encrypted)
83+
84+
decoded, err := DecryptHybridAESRSAEncryptedPayloadFromPrivateKey(testKeyPath, encrypted)
85+
errortest.AssertErrorDescription(t, err, "failed to decode PEM block from certificate")
86+
assert.Empty(t, decoded)
87+
})
88+
89+
t.Run("encrypt: invalid key (missing)", func(t *testing.T) {
90+
testCertPath := filepath.Join(strings.Split(faker.Sentence(), " ")...)
91+
content := []byte(faker.Sentence())
92+
require.NoFileExists(t, testCertPath)
93+
94+
encrypted, err := EncryptHybridAESRSAEncryptedPayloadFromCertificate(testCertPath, content)
95+
errortest.AssertErrorDescription(t, err, "could not find certificate")
96+
assert.Nil(t, encrypted)
97+
})
98+
99+
t.Run("encrypt: invalid key (invalid file)", func(t *testing.T) {
100+
testCertPath := filepath.Join(t.TempDir(), faker.Word())
101+
err := filesystem.WriteFile(testCertPath, []byte(faker.Sentence()), 0644)
102+
require.NoError(t, err)
103+
require.FileExists(t, testCertPath)
104+
105+
content := []byte(faker.Sentence())
106+
107+
encrypted, err := EncryptHybridAESRSAEncryptedPayloadFromCertificate(testCertPath, content)
108+
errortest.AssertErrorDescription(t, err, "failed to decode PEM block from certificate")
109+
assert.Nil(t, encrypted)
110+
})
111+
}

utils/encryption/aesrsa/payload.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package aesrsa
2+
3+
import (
4+
"encoding/base64"
5+
"fmt"
6+
7+
validation "github.com/go-ozzo/ozzo-validation/v4"
8+
9+
"github.com/ARM-software/golang-utils/utils/commonerrors"
10+
"github.com/ARM-software/golang-utils/utils/config"
11+
)
12+
13+
type HybridAESRSAEncryptedPayload struct {
14+
// Ciphertext contains the encrypted contents
15+
CipherText string `json:"cipher_text" yaml:"cipher_text" mapstructure:"cipher_text"`
16+
// EncryptedKey contains the encryped AES key used to encrypt the data
17+
EncryptedKey string `json:"encrypted_key" yaml:"encrypted_key" mapstructure:"encrypted_key"`
18+
// Nonce used for encryption is required during decryption
19+
Nonce string `json:"nonce" yaml:"nonce" mapstructure:"nonce"`
20+
}
21+
22+
func (p *HybridAESRSAEncryptedPayload) Validate() (err error) {
23+
err = config.ValidateEmbedded(p)
24+
if err != nil {
25+
return err
26+
}
27+
28+
return validation.ValidateStruct(p,
29+
validation.Field(&p.CipherText, validation.Required),
30+
validation.Field(&p.EncryptedKey, validation.Required),
31+
validation.Field(&p.Nonce, validation.Required),
32+
)
33+
}
34+
35+
func DecodeHybridAESRSAEncryptedPayload(p *HybridAESRSAEncryptedPayload) (cipher, key, nonce []byte, err error) {
36+
key, err = base64.StdEncoding.DecodeString(p.EncryptedKey)
37+
if err != nil {
38+
err = fmt.Errorf("%w: could not decode base64 encoded encrypted key %v", commonerrors.ErrUnexpected, err.Error())
39+
return
40+
}
41+
42+
nonce, err = base64.StdEncoding.DecodeString(p.Nonce)
43+
if err != nil {
44+
err = fmt.Errorf("%w: could not decode base64 encoded nonce %v", commonerrors.ErrUnexpected, err.Error())
45+
return
46+
}
47+
48+
cipher, err = base64.StdEncoding.DecodeString(p.CipherText)
49+
if err != nil {
50+
err = fmt.Errorf("%w: could not decode base64 encoded ciphertext %v", commonerrors.ErrUnexpected, err.Error())
51+
return
52+
}
53+
54+
return
55+
}
56+
57+
func EncodeHybridAESRSAEncryptedPayload(cipher, key, nonce []byte) (p *HybridAESRSAEncryptedPayload) {
58+
return &HybridAESRSAEncryptedPayload{
59+
EncryptedKey: base64.StdEncoding.EncodeToString(key),
60+
Nonce: base64.StdEncoding.EncodeToString(nonce),
61+
CipherText: base64.StdEncoding.EncodeToString(cipher),
62+
}
63+
}

0 commit comments

Comments
 (0)