Welcome to mirror list, hosted at ThFree Co, Russian Federation.

auth_code.go « auth « internal - gitlab.com/gitlab-org/gitlab-pages.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: f53aec9d11ffa1852d4a16e20e39b3f9766b5247 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package auth

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"fmt"
	"io"

	"github.com/golang-jwt/jwt/v5"
	"github.com/gorilla/securecookie"
	"golang.org/x/crypto/hkdf"
)

var (
	errInvalidToken      = errors.New("invalid token")
	errEmptyDomainOrCode = errors.New("empty domain or code")
	errInvalidNonce      = errors.New("invalid nonce")
	errInvalidCode       = errors.New("invalid code")
)

// EncryptAndSignCode encrypts the OAuth code deriving the key from the domain.
// It adds the code and domain as JWT token claims and signs it using signingKey derived from
// the Auth secret.
func (a *Auth) EncryptAndSignCode(domain, code string) (string, error) {
	if domain == "" || code == "" {
		return "", errEmptyDomainOrCode
	}

	// for FIPS mode, the nonce size has to be equal to the gcmStandardNonceSize i.e. 12
	// https://gitlab.com/gitlab-org/gitlab-pages/-/issues/726
	nonce := securecookie.GenerateRandomKey(12)
	if nonce == nil {
		// https://github.com/gorilla/securecookie/blob/f37875ef1fb538320ab97fc6c9927d94c280ed5b/securecookie.go#L513
		return "", errInvalidNonce
	}

	aesGcm, err := a.newAesGcmCipher(domain, nonce)
	if err != nil {
		return "", err
	}

	// encrypt code with a randomly generated nonce
	encryptedCode := aesGcm.Seal(nil, nonce, []byte(code), nil)

	// generate JWT token claims with encrypted code
	claims := jwt.MapClaims{
		// standard claims
		"iss": "gitlab-pages",
		"iat": a.now().Unix(),
		"exp": a.now().Add(a.jwtExpiry).Unix(),
		// custom claims
		"domain": domain, // pass the domain so we can validate the signed domain matches the requested domain
		"code":   hex.EncodeToString(encryptedCode),
		"nonce":  base64.URLEncoding.EncodeToString(nonce),
	}

	return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(a.jwtSigningKey)
}

// DecryptCode decodes the secureCode as a JWT token and validates its signature.
// It then decrypts the code from the token claims and returns it.
func (a *Auth) DecryptCode(jwt, domain string) (string, error) {
	claims, err := a.parseJWTClaims(jwt)
	if err != nil {
		return "", err
	}

	// get nonce and encryptedCode from the JWT claims
	encodedNonce, ok := claims["nonce"].(string)
	if !ok {
		return "", errInvalidNonce
	}

	nonce, err := base64.URLEncoding.DecodeString(encodedNonce)
	if err != nil {
		return "", errInvalidNonce
	}

	encryptedCode, ok := claims["code"].(string)
	if !ok {
		return "", errInvalidCode
	}

	cipherText, err := hex.DecodeString(encryptedCode)
	if err != nil {
		return "", err
	}

	aesGcm, err := a.newAesGcmCipher(domain, nonce)
	if err != nil {
		return "", err
	}

	decryptedCode, err := aesGcm.Open(nil, nonce, cipherText, nil)
	if err != nil {
		return "", err
	}

	return string(decryptedCode), nil
}

func (a *Auth) codeKey(domain string) ([]byte, error) {
	hkdfReader := hkdf.New(sha256.New, []byte(a.authSecret), []byte(domain), []byte("PAGES_AUTH_CODE_ENCRYPTION_KEY"))

	key := make([]byte, 32)
	if _, err := io.ReadFull(hkdfReader, key); err != nil {
		return nil, err
	}

	return key, nil
}

func (a *Auth) parseJWTClaims(secureCode string) (jwt.MapClaims, error) {
	token, err := jwt.Parse(secureCode, a.getSigningKey)
	if err != nil {
		return nil, err
	}

	claims, ok := token.Claims.(jwt.MapClaims)
	if !ok || !token.Valid {
		return nil, errInvalidToken
	}

	return claims, nil
}

func (a *Auth) getSigningKey(token *jwt.Token) (interface{}, error) {
	// Don't forget to validate the alg is what you expect:
	if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
		return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
	}

	return a.jwtSigningKey, nil
}

func (a *Auth) newAesGcmCipher(domain string, nonce []byte) (cipher.AEAD, error) {
	// get the same key for a domain
	key, err := a.codeKey(domain)
	if err != nil {
		return nil, err
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	aesGcm, err := cipher.NewGCMWithNonceSize(block, len(nonce))
	if err != nil {
		return nil, err
	}

	return aesGcm, nil
}