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
}
|