Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ assets:

test:
@(go test -race -v github.com/minio/mcs/restapi/...)
@(go test -race -v github.com/minio/mcs/pkg/auth)

coverage:
@(go test -v -coverprofile=coverage.out github.com/minio/mcs/restapi/... && go tool cover -html=coverage.out && open coverage.html)
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ $ mc admin policy set myminio mcsAdmin user=mcs
To run the server:

```
export MCS_HMAC_JWT_SECRET=YOURJWTSIGNINGSECRET

#required to encrypt jwet payload
export MCS_PBKDF_PASSPHRASE=SECRET

#required to encrypt jwet payload
export MCS_PBKDF_SALT=SECRET

export MCS_ACCESS_KEY=mcs
export MCS_SECRET_KEY=YOURMCSSECRET
export MCS_MINIO_SERVER=http://localhost:9000
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/minio/mcs
go 1.14

require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/elazarl/go-bindata-assetfs v1.0.0
github.com/go-openapi/errors v0.19.4
github.com/go-openapi/loads v0.19.5
Expand All @@ -12,11 +13,14 @@ require (
github.com/go-openapi/swag v0.19.8
github.com/go-openapi/validate v0.19.7
github.com/jessevdk/go-flags v1.4.0
github.com/json-iterator/go v1.1.9
github.com/minio/cli v1.22.0
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab
github.com/minio/minio-go/v6 v6.0.53
github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.5.1
github.com/unrolled/secure v1.0.7
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab h1:9hlqghJl3e3HorXa6AD
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab/go.mod h1:v8oQPMMaTkjDwp5cOz1WCElA4Ik+X+0y4On+VMk0fis=
github.com/minio/minio-go/v6 v6.0.53 h1:8jzpwiOzZ5Iz7/goFWqNZRICbyWYShbb5rARjrnSCNI=
github.com/minio/minio-go/v6 v6.0.53/go.mod h1:DIvC/IApeHX8q1BAMVCXSXwpmrmM+I+iBvhvztQorfI=
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61 h1:pUSI/WKPdd77gcuoJkSzhJ4wdS8OMDOsOu99MtpXEQA=
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61/go.mod h1:4trzEJ7N1nBTd5Tt7OCZT5SEin+WiAXpdJ/WgPkESA8=
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
Expand Down Expand Up @@ -494,6 +495,7 @@ github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/secure-io/sio-go v0.3.0 h1:QKGb6rGJeiExac9wSWxnWPYo8O8OFN7lxXQvHshX6vo=
github.com/secure-io/sio-go v0.3.0/go.mod h1:D3KmXgKETffyYxBdFRN+Hpd2WzhzqS0EQwT3XWsAcBU=
Expand Down
180 changes: 180 additions & 0 deletions pkg/auth/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package auth

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"errors"
"fmt"
"io"
"log"
"strings"

jwtgo "github.com/dgrijalva/jwt-go"
xjwt "github.com/minio/mcs/pkg/auth/jwt"
"github.com/minio/minio-go/v6/pkg/credentials"
"github.com/minio/minio/cmd"
uuid "github.com/satori/go.uuid"
"golang.org/x/crypto/pbkdf2"
)

var (
errAuthentication = errors.New("Authentication failed, check your access credentials")
errNoAuthToken = errors.New("JWT token missing")
errReadingToken = errors.New("JWT internal data is malformed")
errClaimsFormat = errors.New("encrypted jwt claims not in the right format")
)

// derivedKey is the key used to encrypt the JWT claims, its derived using pbkdf on MCS_PBKDF_PASSPHRASE with MCS_PBKDF_SALT
var derivedKey = pbkdf2.Key([]byte(xjwt.GetPBKDFPassphrase()), []byte(xjwt.GetPBKDFSalt()), 4096, 32, sha1.New)

// IsJWTValid returns true or false depending if the provided jwt is valid or not
func IsJWTValid(token string) bool {
_, err := JWTAuthenticate(token)
return err == nil
}

type DecryptedClaims struct {
AccessKeyID string
SecretAccessKey string
SessionToken string
}

// JWTAuthenticate takes a jwt, decode it, extract claims and validate the signature
// if the jwt claims.Data is valid we proceed to decrypt the information inside
//
// returns claims after validation in the following format:
//
// type DecryptedClaims struct {
// AccessKeyID
// SecretAccessKey
// SessionToken
// }
func JWTAuthenticate(token string) (*DecryptedClaims, error) {
if token == "" {
return nil, errNoAuthToken
}
// initialize claims object
claims := xjwt.NewMapClaims()
// populate the claims object
if err := xjwt.ParseWithClaims(token, claims); err != nil {
return nil, errAuthentication
}
// decrypt the claims.Data field
claimTokens, err := decryptClaims(claims.Data)
if err != nil {
// we print decryption token error information for debugging purposes
log.Println(err)
// we return a generic error that doesn't give any information to attackers
return nil, errReadingToken
}
// claimsTokens contains the decrypted STS claims
return claimTokens, nil
}

// NewJWTWithClaimsForClient generates a new jwt with claims based on the provided STS credentials, first
// encrypts the claims and the sign them
func NewJWTWithClaimsForClient(credentials *credentials.Value, audience string) (string, error) {
if credentials != nil {
encryptedClaims, err := encryptClaims(credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken)
if err != nil {
return "", err
}
claims := xjwt.NewStandardClaims()
claims.SetExpiry(cmd.UTCNow().Add(xjwt.GetMcsSTSAndJWTDurationTime()))
claims.SetSubject(uuid.NewV4().String())
claims.SetData(encryptedClaims)
claims.SetAudience(audience)
jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims)
return jwt.SignedString([]byte(xjwt.GetHmacJWTSecret()))
}
return "", errors.New("provided credentials are empty")
}

// encryptClaims() receives the 3 STS claims, concatenate them and encrypt them using AES-GCM
// returns a base64 encoded ciphertext
func encryptClaims(accessKeyID, secretAccessKey, sessionToken string) (string, error) {
payload := []byte(fmt.Sprintf("%s:%s:%s", accessKeyID, secretAccessKey, sessionToken))
ciphertext, err := encrypt(payload)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}

// decryptClaims() receives base64 encoded ciphertext, decode it, decrypt it (AES-GCM) and produces a *DecryptedClaims object
func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
decoded, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
log.Println(err)
return nil, errClaimsFormat
}
plaintext, err := decrypt(decoded)
if err != nil {
log.Println(err)
return nil, errClaimsFormat
}
s := strings.Split(string(plaintext), ":")
// Validate that the decrypted string has the right format "accessKeyID:secretAccessKey:sessionToken"
if len(s) != 3 {
return nil, errClaimsFormat
}
accessKeyID, secretAccessKey, sessionToken := s[0], s[1], s[2]
return &DecryptedClaims{
AccessKeyID: accessKeyID,
SecretAccessKey: secretAccessKey,
SessionToken: sessionToken,
}, nil
}

// Encrypt a blob of data using AEAD (AES-GCM) with a pbkdf2 derived key
func encrypt(plaintext []byte) ([]byte, error) {
block, _ := aes.NewCipher(derivedKey)
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
cipherText := gcm.Seal(nonce, nonce, plaintext, nil)
return cipherText, nil
}

// Decrypts a blob of data using AEAD (AES-GCM) with a pbkdf2 derived key
func decrypt(data []byte) ([]byte, error) {
block, err := aes.NewCipher(derivedKey)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
nonce, cipherText := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, cipherText, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
87 changes: 43 additions & 44 deletions restapi/sessions/sessions.go → pkg/auth/jwt/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,58 +14,18 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package sessions
package jwt

import (
"crypto/rand"
"io"
"strconv"
"strings"
"sync"
"time"

mcCmd "github.com/minio/mc/cmd"
"github.com/minio/minio/pkg/env"
)

type Singleton struct {
sessions map[string]*mcCmd.Config
}

var instance *Singleton
var once sync.Once

// Returns a Singleton instance that keeps the sessions
func GetInstance() *Singleton {
once.Do(func() {
//build sessions hash
sessions := make(map[string]*mcCmd.Config)

instance = &Singleton{
sessions: sessions,
}
})
return instance
}

// The delete built-in function deletes the element with the specified key (m[key]) from the map.
// If m is nil or there is no such element, delete is a no-op. https://golang.org/pkg/builtin/#delete
func (s *Singleton) DeleteSession(sessionID string) {
delete(s.sessions, sessionID)
}

func (s *Singleton) NewSession(cfg *mcCmd.Config) string {
// genereate random session id
sessionID := RandomCharString(64)
// store the cfg under that session id
s.sessions[sessionID] = cfg
return sessionID
}

func (s *Singleton) ValidSession(sessionID string) bool {
if _, ok := s.sessions[sessionID]; ok {
return true
}
return false
}

// Do not use:
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
// It relies on math/rand and therefore not on a cryptographically secure RNG => It must not be used
Expand Down Expand Up @@ -93,3 +53,42 @@ func RandomCharString(n int) string {
}
return s.String()
}

// defaultHmacJWTPassphrase will be used by default if application is not configured with a custom MCS_HMAC_JWT_SECRET secret
var defaultHmacJWTPassphrase = RandomCharString(64)

// GetHmacJWTSecret returns the 64 bytes secret used for signing the generated JWT for the application
func GetHmacJWTSecret() string {
return env.Get(McsHmacJWTSecret, defaultHmacJWTPassphrase)
}

// McsSTSAndJWTDurationSeconds returns the default session duration for the STS requested tokens and the generated JWTs.
// Ideally both values should match so jwt and Minio sts sessions expires at the same time.
func GetMcsSTSAndJWTDurationInSeconds() int {
duration, err := strconv.Atoi(env.Get(McsSTSAndJWTDurationSeconds, "3600"))
if err != nil {
duration = 3600
}
return duration
}

// GetMcsSTSAndJWTDurationTime returns GetMcsSTSAndJWTDurationInSeconds in duration format
func GetMcsSTSAndJWTDurationTime() time.Duration {
duration := GetMcsSTSAndJWTDurationInSeconds()
return time.Duration(duration) * time.Second
}

// defaultPBKDFPassphrase
var defaultPBKDFPassphrase = RandomCharString(64)

// GetPBKDFPassphrase returns passphrase for the pbkdf2 function used to encrypt JWT payload
func GetPBKDFPassphrase() string {
return env.Get(McsPBKDFPassphrase, defaultPBKDFPassphrase)
}

var defaultPBKDFSalt = RandomCharString(64)

// GetPBKDFSalt returns salt for the pbkdf2 function used to encrypt JWT payload
func GetPBKDFSalt() string {
return env.Get(McsPBKDFSalt, defaultPBKDFSalt)
}
24 changes: 24 additions & 0 deletions pkg/auth/jwt/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package jwt

const (
McsHmacJWTSecret = "MCS_HMAC_JWT_SECRET"
McsSTSAndJWTDurationSeconds = "MCS_STS_AND_JWT_DURATION_SECONDS"
McsPBKDFPassphrase = "MCS_PBKDF_PASSPHRASE"
McsPBKDFSalt = "MCS_PBKDF_SALT"
)
Loading