encrypt token session using aes-gcm if cpu support it or ChaCha20 (#248)

Harsha's improvement to use binary encoding instead of json encoding
This commit is contained in:
Lenin Alevski
2020-08-18 12:42:13 -07:00
committed by GitHub
parent 25fa2f3275
commit f880e3976f
3 changed files with 135 additions and 26 deletions

1
go.mod
View File

@@ -21,6 +21,7 @@ require (
github.com/minio/minio-go/v7 v7.0.5-0.20200807085956-d7db33ea7618 github.com/minio/minio-go/v7 v7.0.5-0.20200807085956-d7db33ea7618
github.com/minio/operator v0.0.0-20200806194125-c2ff646f4af1 github.com/minio/operator v0.0.0-20200806194125-c2ff646f4af1
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/secure-io/sio-go v0.3.1
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
github.com/unrolled/secure v1.0.7 github.com/unrolled/secure v1.0.7
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de

View File

@@ -17,14 +17,17 @@
package auth package auth
import ( import (
"bytes"
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"crypto/rand" "crypto/hmac"
"crypto/sha1" "crypto/sha1"
"crypto/sha256"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"strings" "strings"
@@ -33,6 +36,9 @@ import (
"github.com/minio/console/models" "github.com/minio/console/models"
"github.com/minio/console/pkg/auth/token" "github.com/minio/console/pkg/auth/token"
"github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/credentials"
"github.com/secure-io/sio-go/sioutil"
"golang.org/x/crypto/chacha20"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/pbkdf2"
) )
@@ -40,6 +46,7 @@ var (
errNoAuthToken = errors.New("session token missing") errNoAuthToken = errors.New("session token missing")
errReadingToken = errors.New("session token internal data is malformed") errReadingToken = errors.New("session token internal data is malformed")
errClaimsFormat = errors.New("encrypted session token claims not in the right format") errClaimsFormat = errors.New("encrypted session token claims not in the right format")
errorGeneric = errors.New("an error has occurred")
) )
// derivedKey is the key used to encrypt the session token claims, its derived using pbkdf on CONSOLE_PBKDF_PASSPHRASE with CONSOLE_PBKDF_SALT // derivedKey is the key used to encrypt the session token claims, its derived using pbkdf on CONSOLE_PBKDF_PASSPHRASE with CONSOLE_PBKDF_SALT
@@ -102,9 +109,10 @@ func NewEncryptedTokenForClient(credentials *credentials.Value, actions []string
// returns a base64 encoded ciphertext // returns a base64 encoded ciphertext
func encryptClaims(accessKeyID, secretAccessKey, sessionToken string, actions []string) (string, error) { func encryptClaims(accessKeyID, secretAccessKey, sessionToken string, actions []string) (string, error) {
payload := []byte(fmt.Sprintf("%s#%s#%s#%s", accessKeyID, secretAccessKey, sessionToken, strings.Join(actions, ","))) payload := []byte(fmt.Sprintf("%s#%s#%s#%s", accessKeyID, secretAccessKey, sessionToken, strings.Join(actions, ",")))
ciphertext, err := encrypt(payload) ciphertext, err := encrypt(payload, []byte{})
if err != nil { if err != nil {
return "", err log.Println(err)
return "", errorGeneric
} }
return base64.StdEncoding.EncodeToString(ciphertext), nil return base64.StdEncoding.EncodeToString(ciphertext), nil
} }
@@ -116,7 +124,7 @@ func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
log.Println(err) log.Println(err)
return nil, errClaimsFormat return nil, errClaimsFormat
} }
plaintext, err := decrypt(decoded) plaintext, err := decrypt(decoded, []byte{})
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return nil, errClaimsFormat return nil, errClaimsFormat
@@ -136,37 +144,137 @@ func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
}, nil }, nil
} }
// Encrypt a blob of data using AEAD (AES-GCM) with a pbkdf2 derived key const (
func encrypt(plaintext []byte) ([]byte, error) { aesGcm = 0x00
block, _ := aes.NewCipher(derivedKey) c20p1305 = 0x01
gcm, err := cipher.NewGCM(block) )
// Encrypt a blob of data using AEAD scheme, AES-GCM if the executing CPU
// provides AES hardware support, otherwise will use ChaCha20-Poly1305
// with a pbkdf2 derived key, this function should be used to encrypt a session
// or data key provided as plaintext.
//
// The returned ciphertext data consists of:
// iv | AEAD ID | nonce | encrypted data
// 32 1 12 ~ len(data)
func encrypt(plaintext, associatedData []byte) ([]byte, error) {
iv, err := sioutil.Random(32) // 32 bit IV
if err != nil { if err != nil {
return nil, err return nil, err
} }
nonce := make([]byte, gcm.NonceSize()) var algorithm byte
if _, err = io.ReadFull(rand.Reader, nonce); err != nil { if sioutil.NativeAES() {
algorithm = aesGcm
} else {
algorithm = c20p1305
}
var aead cipher.AEAD
switch algorithm {
case aesGcm:
mac := hmac.New(sha256.New, derivedKey)
mac.Write(iv)
sealingKey := mac.Sum(nil)
var block cipher.Block
block, err = aes.NewCipher(sealingKey)
if err != nil {
return nil, err
}
aead, err = cipher.NewGCM(block)
if err != nil {
return nil, err
}
case c20p1305:
var sealingKey []byte
sealingKey, err = chacha20.HChaCha20(derivedKey, iv)
if err != nil {
return nil, err
}
aead, err = chacha20poly1305.New(sealingKey)
if err != nil {
return nil, err
}
}
nonce, err := sioutil.Random(aead.NonceSize())
if err != nil {
return nil, err return nil, err
} }
cipherText := gcm.Seal(nonce, nonce, plaintext, nil)
return cipherText, nil sealedBytes := aead.Seal(nil, nonce, plaintext, associatedData)
// ciphertext = iv | AEAD ID | nonce | sealed bytes
var buf bytes.Buffer
buf.Write(iv)
buf.WriteByte(algorithm)
buf.Write(nonce)
buf.Write(sealedBytes)
return buf.Bytes(), nil
} }
// Decrypts a blob of data using AEAD (AES-GCM) with a pbkdf2 derived key // Decrypts a blob of data using AEAD scheme AES-GCM if the executing CPU
func decrypt(data []byte) ([]byte, error) { // provides AES hardware support, otherwise will use ChaCha20-Poly1305with
block, err := aes.NewCipher(derivedKey) // and a pbkdf2 derived key
func decrypt(ciphertext []byte, associatedData []byte) ([]byte, error) {
var (
iv [32]byte
algorithm [1]byte
nonce [12]byte // This depends on the AEAD but both used ciphers have the same nonce length.
)
r := bytes.NewReader(ciphertext)
if _, err := io.ReadFull(r, iv[:]); err != nil {
return nil, err
}
if _, err := io.ReadFull(r, algorithm[:]); err != nil {
return nil, err
}
if _, err := io.ReadFull(r, nonce[:]); err != nil {
return nil, err
}
var aead cipher.AEAD
switch algorithm[0] {
case aesGcm:
mac := hmac.New(sha256.New, derivedKey)
mac.Write(iv[:])
sealingKey := mac.Sum(nil)
block, err := aes.NewCipher(sealingKey[:])
if err != nil {
return nil, err
}
aead, err = cipher.NewGCM(block)
if err != nil {
return nil, err
}
case c20p1305:
sealingKey, err := chacha20.HChaCha20(derivedKey, iv[:])
if err != nil {
return nil, err
}
aead, err = chacha20poly1305.New(sealingKey)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("invalid algorithm: %v", algorithm)
}
if len(nonce) != aead.NonceSize() {
return nil, fmt.Errorf("invalid nonce size %d, expected %d", len(nonce), aead.NonceSize())
}
sealedBytes, err := ioutil.ReadAll(r)
if err != nil { if err != nil {
return nil, err return nil, err
} }
gcm, err := cipher.NewGCM(block)
if err != nil { plaintext, err := aead.Open(nil, nonce[:], sealedBytes, associatedData)
return nil, err
}
nonceSize := gcm.NonceSize()
nonce, cipherText := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, cipherText, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return plaintext, nil return plaintext, nil
} }

View File

@@ -36,12 +36,12 @@ func TestNewJWTWithClaimsForClient(t *testing.T) {
funcAssert := assert.New(t) funcAssert := assert.New(t)
// Test-1 : NewEncryptedTokenForClient() is generated correctly without errors // Test-1 : NewEncryptedTokenForClient() is generated correctly without errors
function := "NewEncryptedTokenForClient()" function := "NewEncryptedTokenForClient()"
jwt, err := NewEncryptedTokenForClient(creds, []string{""}) token, err := NewEncryptedTokenForClient(creds, []string{""})
if err != nil || jwt == "" { if err != nil || token == "" {
t.Errorf("Failed on %s:, error occurred: %s", function, err) t.Errorf("Failed on %s:, error occurred: %s", function, err)
} }
// saving jwt for future tests // saving token for future tests
goodToken = jwt goodToken = token
// Test-2 : NewEncryptedTokenForClient() throws error because of empty credentials // Test-2 : NewEncryptedTokenForClient() throws error because of empty credentials
if _, err = NewEncryptedTokenForClient(nil, []string{""}); err != nil { if _, err = NewEncryptedTokenForClient(nil, []string{""}); err != nil {
funcAssert.Equal("provided credentials are empty", err.Error()) funcAssert.Equal("provided credentials are empty", err.Error())