-
Notifications
You must be signed in to change notification settings - Fork 4
/
v2_local.go
155 lines (118 loc) · 4.7 KB
/
v2_local.go
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
package pvx
import (
"crypto/rand"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/blake2b"
"golang.org/x/crypto/chacha20poly1305"
)
const nonceLen = 24
const (
headerVersion = "v2"
headerPurposeLocal = "local"
headerV2Local = "v2.local."
)
// ErrMalformedToken indicates that obtained token was not properly formed
var ErrMalformedToken = errors.New("token is malformed")
// SymmetricKey is used in encryption and decryption routines.
type SymmetricKey []byte
// PV2Local can be used as a global reference for protocol version 2 with local purpose.
var PV2Local = NewPV2Local()
// ProtoV2Local is a protocol version 2 with local purpose.
type ProtoV2Local struct {
testNonce []byte // for unit testing purposes
}
// NewPV2Local is a constructor-like sugar for protocol 2 version local purpose.
func NewPV2Local() *ProtoV2Local {
return &ProtoV2Local{}
}
// Encrypt encrypts claims with provided symmetric key and authenticates footer,
// protecting it from tampering but preserving it in base64 encoded plaintext.
func (pv2 *ProtoV2Local) Encrypt(key SymmetricKey, claims Claims, footer interface{}) (string, error) {
payload, optionalFooter, err := encode(claims, footer)
if err != nil {
return "", err
}
return pv2.encrypt(key, payload, optionalFooter)
}
// EncryptFooterNil is a sugar function which eliminates optional footer from function parameter list
// Equivalent to Encrypt (key, claims, nil).
func (pv2 *ProtoV2Local) EncryptFooterNil(key SymmetricKey, claims Claims) (string, error) {
return pv2.Encrypt(key, claims, nil)
}
// encrypt is a step-by-step algorithm implemented according to RFC.
func (pv2 *ProtoV2Local) encrypt(key SymmetricKey, message []byte, optionalFooter []byte) (string, error) {
const header = headerV2Local
randomBytes := make([]byte, nonceLen)
_, err := rand.Read(randomBytes)
if err != nil {
return "", fmt.Errorf("rand.Read problem: %w", err)
}
// this is supplementary and not exposed as a public API (for testing purposes only)
// it is about replacing random bytes with specified in advance value if we called this from test
if pv2.testNonce != nil {
randomBytes = pv2.testNonce
}
hash, err := blake2b.New(nonceLen, randomBytes)
if err != nil {
return "", fmt.Errorf("blake2b.New hash problem: %w", err)
}
if _, err := hash.Write(message); err != nil {
return "", fmt.Errorf("failed to hash payload: %w", err)
}
nonce := hash.Sum(nil)
additionalData := preAuthenticationEncoding([]byte(header), nonce, optionalFooter)
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return "", fmt.Errorf("failed to create chacha20poly1305 aead: %w", err)
}
cipherText := aead.Seal(message[:0], nonce, message, additionalData)
nonceWithCipherText := make([]byte, len(nonce)+len(cipherText))
offset := 0
offset += copy(nonceWithCipherText[offset:], nonce)
copy(nonceWithCipherText[offset:], cipherText)
b64NonceAndCipherText := b64(nonceWithCipherText)
emptyFooter := len(optionalFooter) == 0
var b64Footer string
if !emptyFooter {
b64Footer = b64(optionalFooter)
}
var token string
if emptyFooter {
token = strings.Join([]string{headerVersion, headerPurposeLocal, b64NonceAndCipherText}, ".")
} else {
token = strings.Join([]string{headerVersion, headerPurposeLocal, b64NonceAndCipherText, b64Footer}, ".")
}
return token, nil
}
// Decrypt implements PASETO v2.Decrypt returning Token struct ready for subsequent scan in case of success.
func (pv2 *ProtoV2Local) Decrypt(token string, key SymmetricKey) *Token {
plaintextClaims, footer, err := pv2.decrypt(token, key)
return &Token{claims: plaintextClaims, footer: footer, err: err}
}
// decrypt implements PASETO v2.Decrypt returning claims and footer in plaintext
func (pv2 *ProtoV2Local) decrypt(token string, key SymmetricKey) ([]byte, []byte, error) {
if !strings.HasPrefix(token, headerV2Local) {
return nil, nil, fmt.Errorf("decrypted token does not have header v2 local prefix: %w", ErrMalformedToken)
}
bodyBytes, footerBytes, err := decodeB64ToRawBinary(token, len(headerV2Local))
if err != nil {
return nil, nil, fmt.Errorf("failed to decode token: %w", err)
}
if len(bodyBytes) < nonceLen {
return nil, nil, fmt.Errorf("incorrect token size: %w", ErrMalformedToken)
}
nonce := bodyBytes[:nonceLen]
cipherText := bodyBytes[nonceLen:]
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return nil, nil, fmt.Errorf("failed to create chachapoly cipher: %w", err)
}
additionalData := preAuthenticationEncoding([]byte(headerV2Local), nonce, footerBytes)
plainTextClaims, err := aead.Open(cipherText[:0], nonce, cipherText, additionalData)
if err != nil {
return nil, nil, fmt.Errorf("problem while trying to decrypt token: %w", err)
}
return plainTextClaims, footerBytes, nil
}