github.com/minio/console@v1.4.1/pkg/auth/token.go (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2021 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package auth
    18  
    19  import (
    20  	"bytes"
    21  	"crypto/aes"
    22  	"crypto/cipher"
    23  	"crypto/hmac"
    24  	"crypto/sha1"
    25  	"crypto/sha256"
    26  	"encoding/base64"
    27  	"encoding/json"
    28  	"errors"
    29  	"fmt"
    30  	"io"
    31  	"net/http"
    32  	"strings"
    33  	"time"
    34  
    35  	"github.com/minio/console/models"
    36  	"github.com/minio/console/pkg/auth/token"
    37  	"github.com/minio/minio-go/v7/pkg/credentials"
    38  	"github.com/secure-io/sio-go/sioutil"
    39  	"golang.org/x/crypto/chacha20"
    40  	"golang.org/x/crypto/chacha20poly1305"
    41  	"golang.org/x/crypto/pbkdf2"
    42  )
    43  
    44  // Session token errors
    45  var (
    46  	ErrNoAuthToken  = errors.New("session token missing")
    47  	ErrTokenExpired = errors.New("session token has expired")
    48  	ErrReadingToken = errors.New("session token internal data is malformed")
    49  )
    50  
    51  // derivedKey is the key used to encrypt the session token claims, its derived using pbkdf on CONSOLE_PBKDF_PASSPHRASE with CONSOLE_PBKDF_SALT
    52  var derivedKey = func() []byte {
    53  	return pbkdf2.Key([]byte(token.GetPBKDFPassphrase()), []byte(token.GetPBKDFSalt()), 4096, 32, sha1.New)
    54  }
    55  
    56  // IsSessionTokenValid returns true or false depending upon the provided session if the token is valid or not
    57  func IsSessionTokenValid(token string) bool {
    58  	_, err := SessionTokenAuthenticate(token)
    59  	return err == nil
    60  }
    61  
    62  // TokenClaims claims struct for decrypted credentials
    63  type TokenClaims struct {
    64  	STSAccessKeyID     string `json:"stsAccessKeyID,omitempty"`
    65  	STSSecretAccessKey string `json:"stsSecretAccessKey,omitempty"`
    66  	STSSessionToken    string `json:"stsSessionToken,omitempty"`
    67  	AccountAccessKey   string `json:"accountAccessKey,omitempty"`
    68  	HideMenu           bool   `json:"hm,omitempty"`
    69  	ObjectBrowser      bool   `json:"ob,omitempty"`
    70  	CustomStyleOB      string `json:"customStyleOb,omitempty"`
    71  }
    72  
    73  // STSClaims claims struct for STS Token
    74  type STSClaims struct {
    75  	AccessKey string `json:"accessKey,omitempty"`
    76  }
    77  
    78  // SessionFeatures represents features stored in the session
    79  type SessionFeatures struct {
    80  	HideMenu      bool
    81  	ObjectBrowser bool
    82  	CustomStyleOB string
    83  }
    84  
    85  // SessionTokenAuthenticate takes a session token, decode it, extract claims and validate the signature
    86  // if the session token claims are valid we proceed to decrypt the information inside
    87  //
    88  // returns claims after validation in the following format:
    89  //
    90  //	type TokenClaims struct {
    91  //		STSAccessKeyID
    92  //		STSSecretAccessKey
    93  //		STSSessionToken
    94  //		AccountAccessKey
    95  //	}
    96  func SessionTokenAuthenticate(token string) (*TokenClaims, error) {
    97  	if token == "" {
    98  		return nil, ErrNoAuthToken
    99  	}
   100  	decryptedToken, err := DecryptToken(token)
   101  	if err != nil {
   102  		// fail decrypting token
   103  		return nil, ErrReadingToken
   104  	}
   105  	claimTokens, err := ParseClaimsFromToken(string(decryptedToken))
   106  	if err != nil {
   107  		// fail unmarshalling token into data structure
   108  		return nil, ErrReadingToken
   109  	}
   110  	// claimsTokens contains the decrypted JWT for Console
   111  	return claimTokens, nil
   112  }
   113  
   114  // NewEncryptedTokenForClient generates a new session token with claims based on the provided STS credentials, first
   115  // encrypts the claims and the sign them
   116  func NewEncryptedTokenForClient(credentials *credentials.Value, accountAccessKey string, features *SessionFeatures) (string, error) {
   117  	if credentials != nil {
   118  		tokenClaims := &TokenClaims{
   119  			STSAccessKeyID:     credentials.AccessKeyID,
   120  			STSSecretAccessKey: credentials.SecretAccessKey,
   121  			STSSessionToken:    credentials.SessionToken,
   122  			AccountAccessKey:   accountAccessKey,
   123  		}
   124  		if features != nil {
   125  			tokenClaims.HideMenu = features.HideMenu
   126  			tokenClaims.ObjectBrowser = features.ObjectBrowser
   127  			tokenClaims.CustomStyleOB = features.CustomStyleOB
   128  		}
   129  
   130  		encryptedClaims, err := encryptClaims(tokenClaims)
   131  		if err != nil {
   132  			return "", err
   133  		}
   134  		return encryptedClaims, nil
   135  	}
   136  	return "", errors.New("provided credentials are empty")
   137  }
   138  
   139  // encryptClaims() receives the STS claims, concatenate them and encrypt them using AES-GCM
   140  // returns a base64 encoded ciphertext
   141  func encryptClaims(credentials *TokenClaims) (string, error) {
   142  	payload, err := json.Marshal(credentials)
   143  	if err != nil {
   144  		return "", err
   145  	}
   146  	ciphertext, err := encrypt(payload, []byte{})
   147  	if err != nil {
   148  		return "", err
   149  	}
   150  	return base64.StdEncoding.EncodeToString(ciphertext), nil
   151  }
   152  
   153  // ParseClaimsFromToken receive token claims in string format, then unmarshal them to produce a *TokenClaims object
   154  func ParseClaimsFromToken(claims string) (*TokenClaims, error) {
   155  	tokenClaims := &TokenClaims{}
   156  	if err := json.Unmarshal([]byte(claims), tokenClaims); err != nil {
   157  		return nil, err
   158  	}
   159  	return tokenClaims, nil
   160  }
   161  
   162  // DecryptToken receives base64 encoded ciphertext, decode it, decrypt it (AES-GCM) and produces []byte
   163  func DecryptToken(ciphertext string) (plaintext []byte, err error) {
   164  	decoded, err := base64.StdEncoding.DecodeString(ciphertext)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	plaintext, err = decrypt(decoded, []byte{})
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	return plaintext, nil
   173  }
   174  
   175  const (
   176  	aesGcm   = 0x00
   177  	c20p1305 = 0x01
   178  )
   179  
   180  // Encrypt a blob of data using AEAD scheme, AES-GCM if the executing CPU
   181  // provides AES hardware support, otherwise will use ChaCha20-Poly1305
   182  // with a pbkdf2 derived key, this function should be used to encrypt a session
   183  // or data key provided as plaintext.
   184  //
   185  // The returned ciphertext data consists of:
   186  //
   187  //	AEAD ID | iv | nonce | encrypted data
   188  //	   1      16		 12     ~ len(data)
   189  func encrypt(plaintext, associatedData []byte) ([]byte, error) {
   190  	iv, err := sioutil.Random(16) // 16 bytes IV
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	var algorithm byte
   195  	if sioutil.NativeAES() {
   196  		algorithm = aesGcm
   197  	} else {
   198  		algorithm = c20p1305
   199  	}
   200  	var aead cipher.AEAD
   201  	switch algorithm {
   202  	case aesGcm:
   203  		mac := hmac.New(sha256.New, derivedKey())
   204  		mac.Write(iv)
   205  		sealingKey := mac.Sum(nil)
   206  
   207  		var block cipher.Block
   208  		block, err = aes.NewCipher(sealingKey)
   209  		if err != nil {
   210  			return nil, err
   211  		}
   212  		aead, err = cipher.NewGCM(block)
   213  		if err != nil {
   214  			return nil, err
   215  		}
   216  	case c20p1305:
   217  		var sealingKey []byte
   218  		sealingKey, err = chacha20.HChaCha20(derivedKey(), iv) // HChaCha20 expects nonce of 16 bytes
   219  		if err != nil {
   220  			return nil, err
   221  		}
   222  		aead, err = chacha20poly1305.New(sealingKey)
   223  		if err != nil {
   224  			return nil, err
   225  		}
   226  	}
   227  	nonce, err := sioutil.Random(aead.NonceSize())
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	sealedBytes := aead.Seal(nil, nonce, plaintext, associatedData)
   233  
   234  	// ciphertext = AEAD ID | iv | nonce | sealed bytes
   235  
   236  	var buf bytes.Buffer
   237  	buf.WriteByte(algorithm)
   238  	buf.Write(iv)
   239  	buf.Write(nonce)
   240  	buf.Write(sealedBytes)
   241  
   242  	return buf.Bytes(), nil
   243  }
   244  
   245  // Decrypts a blob of data using AEAD scheme AES-GCM if the executing CPU
   246  // provides AES hardware support, otherwise will use ChaCha20-Poly1305with
   247  // and a pbkdf2 derived key
   248  func decrypt(ciphertext, associatedData []byte) ([]byte, error) {
   249  	var (
   250  		algorithm [1]byte
   251  		iv        [16]byte
   252  		nonce     [12]byte // This depends on the AEAD but both used ciphers have the same nonce length.
   253  	)
   254  
   255  	r := bytes.NewReader(ciphertext)
   256  	if _, err := io.ReadFull(r, algorithm[:]); err != nil {
   257  		return nil, err
   258  	}
   259  	if _, err := io.ReadFull(r, iv[:]); err != nil {
   260  		return nil, err
   261  	}
   262  	if _, err := io.ReadFull(r, nonce[:]); err != nil {
   263  		return nil, err
   264  	}
   265  
   266  	var aead cipher.AEAD
   267  	switch algorithm[0] {
   268  	case aesGcm:
   269  		mac := hmac.New(sha256.New, derivedKey())
   270  		mac.Write(iv[:])
   271  		sealingKey := mac.Sum(nil)
   272  		block, err := aes.NewCipher(sealingKey)
   273  		if err != nil {
   274  			return nil, err
   275  		}
   276  		aead, err = cipher.NewGCM(block)
   277  		if err != nil {
   278  			return nil, err
   279  		}
   280  	case c20p1305:
   281  		sealingKey, err := chacha20.HChaCha20(derivedKey(), iv[:]) // HChaCha20 expects nonce of 16 bytes
   282  		if err != nil {
   283  			return nil, err
   284  		}
   285  		aead, err = chacha20poly1305.New(sealingKey)
   286  		if err != nil {
   287  			return nil, err
   288  		}
   289  	default:
   290  		return nil, fmt.Errorf("invalid algorithm: %v", algorithm)
   291  	}
   292  
   293  	if len(nonce) != aead.NonceSize() {
   294  		return nil, fmt.Errorf("invalid nonce size %d, expected %d", len(nonce), aead.NonceSize())
   295  	}
   296  
   297  	sealedBytes, err := io.ReadAll(r)
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  
   302  	plaintext, err := aead.Open(nil, nonce[:], sealedBytes, associatedData)
   303  	if err != nil {
   304  		return nil, err
   305  	}
   306  
   307  	return plaintext, nil
   308  }
   309  
   310  // GetTokenFromRequest returns a token from a http Request
   311  // either defined on a cookie `token` or on Authorization header.
   312  //
   313  // Authorization Header needs to be like "Authorization Bearer <token>"
   314  func GetTokenFromRequest(r *http.Request) (string, error) {
   315  	// Token might come either as a Cookie or as a Header
   316  	// if not set in cookie, check if it is set on Header.
   317  	tokenCookie, err := r.Cookie("token")
   318  	if err != nil {
   319  		return "", ErrNoAuthToken
   320  	}
   321  	currentTime := time.Now()
   322  	if tokenCookie.Expires.After(currentTime) {
   323  		return "", ErrTokenExpired
   324  	}
   325  	return strings.TrimSpace(tokenCookie.Value), nil
   326  }
   327  
   328  func GetClaimsFromTokenInRequest(req *http.Request) (*models.Principal, error) {
   329  	sessionID, err := GetTokenFromRequest(req)
   330  	if err != nil {
   331  		return nil, err
   332  	}
   333  	// Perform decryption of the session token, if Console is able to decrypt the session token that means a valid session
   334  	// was used in the first place to get it
   335  	claims, err := SessionTokenAuthenticate(sessionID)
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  	return &models.Principal{
   340  		STSAccessKeyID:     claims.STSAccessKeyID,
   341  		STSSecretAccessKey: claims.STSSecretAccessKey,
   342  		STSSessionToken:    claims.STSSessionToken,
   343  		AccountAccessKey:   claims.AccountAccessKey,
   344  	}, nil
   345  }