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 }