Skip to content

Commit

Permalink
✨ Added module [encryption] for simple cryptography utilities (#296)
Browse files Browse the repository at this point in the history
<!--
Copyright (C) 2020-2022 Arm Limited or its affiliates and Contributors.
All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->
### Description

- Added module `[encryption]` for simple cryptography utilities
- Support more hashing algorithm


### 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).
  • Loading branch information
acabarbaye authored Aug 2, 2023
1 parent 70ded98 commit efc7879
Show file tree
Hide file tree
Showing 9 changed files with 428 additions and 18 deletions.
8 changes: 4 additions & 4 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -189,21 +189,21 @@
"filename": "utils/hashing/hash_test.go",
"hashed_secret": "4028a0e356acc947fcd2bfbf00cef11e128d484a",
"is_verified": false,
"line_number": 26
"line_number": 28
},
{
"type": "Hex High Entropy String",
"filename": "utils/hashing/hash_test.go",
"hashed_secret": "1f35be0c58b01a2fddd3aded671f0f7efed3ff62",
"is_verified": false,
"line_number": 29
"line_number": 31
},
{
"type": "Hex High Entropy String",
"filename": "utils/hashing/hash_test.go",
"hashed_secret": "30f0cbefb37316806a7024caee994baf8365fa53",
"is_verified": false,
"line_number": 114
"line_number": 116
}
],
"utils/sharedcache/common.go": [
Expand Down Expand Up @@ -248,5 +248,5 @@
}
]
},
"generated_at": "2023-07-24T13:50:03Z"
"generated_at": "2023-08-01T15:57:07Z"
}
1 change: 1 addition & 0 deletions changes/20230801110107.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: `[hashing]` Added support for [blake2b](https://www.blake2.net/) hashing algorithm
1 change: 1 addition & 0 deletions changes/20230801144705.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: Added module `[encryption]` for simple cryptography utilities
147 changes: 147 additions & 0 deletions utils/encryption/encryption.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package encryption

import (
"crypto/rand"
"encoding/base64"
"fmt"

"golang.org/x/crypto/nacl/box"

"github.com/ARM-software/golang-utils/utils/commonerrors"
)

const KeySize = 32

var (
errKeySize = fmt.Errorf("%w: recipient key has invalid size", commonerrors.ErrInvalid)
)

type keyPair struct {
public string
private string `json:"-"`
}

func (k *keyPair) String() string {
return fmt.Sprintf("{Public: %v}", k.GetPublicKey())
}

func (k *keyPair) GoString() string {
return fmt.Sprintf("KeyPair(%q)", k.String())
}

func (k *keyPair) MarshalJSON() ([]byte, error) {
json := fmt.Sprintf("{\"Public\": %q}", k.GetPublicKey())
return []byte(json), nil
}

func (k *keyPair) GetPublicKey() string {
return k.public
}

func (k *keyPair) GetPrivateKey() string {
return k.private
}

func newKeyPair(public, private *[32]byte) (IKeyPair, error) {
if public == nil || private == nil {
return nil, fmt.Errorf("%w: missing key", commonerrors.ErrUndefined)
}
return newBasicKeyPair(base64.StdEncoding.EncodeToString((*public)[:]), base64.StdEncoding.EncodeToString((*private)[:])), nil
}

func newBasicKeyPair(public, private string) IKeyPair {
return &keyPair{
public: public,
private: private,
}
}

// GenerateKeyPair generates a asymmetric key pair suitable for use with encryption utilities. Works with [NaCl box](https://nacl.cr.yp.to/box.html.)
func GenerateKeyPair() (pair IKeyPair, err error) {
pub, priv, err := box.GenerateKey(rand.Reader)
if err != nil {
err = fmt.Errorf("%w: could not generate keys: %v", commonerrors.ErrUnexpected, err.Error())
return
}

pair, err = newKeyPair(pub, priv)
return
}

// EncryptWithPublicKey encrypts small messages using a 32-byte public key (See https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes)
func EncryptWithPublicKey(base64EncodedPublicKey string, message string) (encryptedBase64Message string, err error) {
decodedPublicKey, err := base64.StdEncoding.DecodeString(base64EncodedPublicKey)
if err != nil {
err = base64DecodingError(err)
return
}
if len(decodedPublicKey) != KeySize {
err = errKeySize
return
}

recipientKey := [KeySize]byte{}
copy(recipientKey[:], decodedPublicKey)

secretBytes := []byte(message)

encryptedBytes, err := box.SealAnonymous([]byte{}, secretBytes, &recipientKey, rand.Reader)
if err != nil {
err = fmt.Errorf("%w: box.SealAnonymous failed with error: %v", commonerrors.ErrUnexpected, err.Error())
return
}

encryptedBase64Message = base64.StdEncoding.EncodeToString(encryptedBytes)

return
}

func base64DecodingError(err error) error {
return fmt.Errorf("%w: base64.StdEncoding.DecodeString was unable to decode string: %v", commonerrors.ErrInvalid, err.Error())
}

// DecryptWithKeyPair decrypts small base64 encoded messages
func DecryptWithKeyPair(keys IKeyPair, base64EncodedEncryptedMessage string) (decryptedMessage string, err error) {
if keys == nil {
err = fmt.Errorf("%w: missing keys", commonerrors.ErrUndefined)
return
}
decodedPublicKey, err := base64.StdEncoding.DecodeString(keys.GetPublicKey())
if err != nil {
err = base64DecodingError(err)
return
}
if len(decodedPublicKey) != KeySize {
err = errKeySize
return
}
decodedPrivateKey, err := base64.StdEncoding.DecodeString(keys.GetPrivateKey())
if err != nil {
err = base64DecodingError(err)
return
}
if len(decodedPrivateKey) != KeySize {
err = errKeySize
return
}

decodedMessage, err := base64.StdEncoding.DecodeString(base64EncodedEncryptedMessage)
if err != nil {
err = base64DecodingError(err)
return
}

publicKey := [KeySize]byte{}
copy(publicKey[:], decodedPublicKey)

privateKey := [KeySize]byte{}
copy(privateKey[:], decodedPrivateKey)

message, ok := box.OpenAnonymous([]byte{}, decodedMessage, &publicKey, &privateKey)
if !ok {
err = fmt.Errorf("%w: message could not be decrypted", commonerrors.ErrInvalid)
return
}
decryptedMessage = string(message)
return
}
96 changes: 96 additions & 0 deletions utils/encryption/encryption_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package encryption

import (
"encoding/base64"
"encoding/json"
"fmt"
"testing"

"github.com/bxcodec/faker/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ARM-software/golang-utils/utils/commonerrors"
"github.com/ARM-software/golang-utils/utils/commonerrors/errortest"
"github.com/ARM-software/golang-utils/utils/logs/logstest"
)

func TestGenerate(t *testing.T) {
pair, err := GenerateKeyPair()
require.NoError(t, err)
assert.NotEmpty(t, pair.GetPublicKey())
assert.NotEmpty(t, pair.GetPrivateKey())

b, err := base64.StdEncoding.DecodeString(pair.GetPublicKey())
require.NoError(t, err)
assert.Equal(t, 32, len(b))
b, err = base64.StdEncoding.DecodeString(pair.GetPrivateKey())
require.NoError(t, err)
assert.Equal(t, 32, len(b))
}

func TestEncryptDecrypt(t *testing.T) {
message := faker.Paragraph()
pair, err := GenerateKeyPair()
require.NoError(t, err)

encrypted, err := EncryptWithPublicKey(pair.GetPublicKey(), message)
require.NoError(t, err)
decryptedMessage, err := DecryptWithKeyPair(pair, encrypted)
require.NoError(t, err)
assert.Equal(t, message, decryptedMessage)
}

func TestKeyPrint(t *testing.T) {
// Test to make sure the private key does not get printing in the logs by mistake.
pair, err := GenerateKeyPair()
require.NoError(t, err)

fmtString := fmt.Sprintf("test: %v", pair)
assert.NotContains(t, fmtString, pair.GetPrivateKey())
fmt.Println(fmtString)

fmtString = fmt.Sprintf("test: %+v", pair)
assert.NotContains(t, fmtString, pair.GetPrivateKey())
fmt.Println(fmtString)

fmtString = fmt.Sprintf("test: %q", pair)
assert.NotContains(t, fmtString, pair.GetPrivateKey())
fmt.Println(fmtString)

fmtJSON, err := json.Marshal(pair)
require.NoError(t, err)
fmtString = string(fmtJSON)
assert.NotContains(t, fmtString, pair.GetPrivateKey())
fmt.Println(fmtString)
logger := logstest.NewTestLogger(t)
logger.Info("test", "key", pair)
}

func TestEncryptDecrypt_Failures(t *testing.T) {
pair, err := GenerateKeyPair()
require.NoError(t, err)

_, err = EncryptWithPublicKey(faker.Name(), faker.Word())
require.Error(t, err)
errortest.AssertError(t, err, commonerrors.ErrInvalid)

invalidPair := newBasicKeyPair(faker.Name(), faker.Name())
_, err = DecryptWithKeyPair(invalidPair, faker.Word())
require.Error(t, err)
errortest.AssertError(t, err, commonerrors.ErrInvalid)

invalidPair = newBasicKeyPair(pair.GetPublicKey(), faker.Name())
_, err = DecryptWithKeyPair(invalidPair, faker.Word())
require.Error(t, err)
errortest.AssertError(t, err, commonerrors.ErrInvalid)

invalidPair = newBasicKeyPair(faker.Name(), pair.GetPrivateKey())
_, err = DecryptWithKeyPair(invalidPair, faker.Word())
require.Error(t, err)
errortest.AssertError(t, err, commonerrors.ErrInvalid)

_, err = DecryptWithKeyPair(pair, faker.Word())
require.Error(t, err)
errortest.AssertError(t, err, commonerrors.ErrInvalid)
}
20 changes: 20 additions & 0 deletions utils/encryption/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Package encryption defines utilities with regards to cryptography.
package encryption

import (
"encoding/json"
"fmt"
)

//go:generate mockgen -destination=../mocks/mock_$GOPACKAGE.go -package=mocks github.com/ARM-software/golang-utils/utils/$GOPACKAGE IKeyPair

// IKeyPair defines an asymmetric key pair for cryptography.
type IKeyPair interface {
fmt.Stringer
fmt.GoStringer
json.Marshaler
// GetPublicKey returns the public key (base64 encoded)
GetPublicKey() string
// GetPrivateKey returns the private key (base64 encoded)
GetPrivateKey() string
}
Loading

0 comments on commit efc7879

Please sign in to comment.