Skip to content

Commit 7ce1c04

Browse files
authored
Implement v1.local (#5)
1 parent 3521250 commit 7ce1c04

File tree

8 files changed

+550
-1
lines changed

8 files changed

+550
-1
lines changed

common.go

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package paseto
2+
3+
import (
4+
"bytes"
5+
"encoding/base64"
6+
"encoding/binary"
7+
"encoding/json"
8+
"fmt"
9+
"strings"
10+
)
11+
12+
func pae(pieces ...[]byte) []byte {
13+
var buf bytes.Buffer
14+
binary.Write(&buf, binary.LittleEndian, int64(len(pieces)))
15+
16+
for _, p := range pieces {
17+
binary.Write(&buf, binary.LittleEndian, int64(len(p)))
18+
buf.Write(p)
19+
}
20+
return buf.Bytes()
21+
}
22+
23+
func toBytes(x any) ([]byte, error) {
24+
switch v := x.(type) {
25+
case nil:
26+
return nil, nil
27+
case string:
28+
return []byte(v), nil
29+
case []byte:
30+
return v, nil
31+
default:
32+
return json.Marshal(v)
33+
}
34+
}
35+
36+
func fromBytes(data []byte, x any) error {
37+
switch f := x.(type) {
38+
case *string:
39+
*f = string(data)
40+
case *[]byte:
41+
*f = append(*f, data...)
42+
default:
43+
if err := json.Unmarshal(data, x); err != nil {
44+
return fmt.Errorf("%v: %w", err, ErrDataUnmarshal)
45+
}
46+
}
47+
return nil
48+
}
49+
50+
func splitToken(token, header string) ([]byte, []byte, error) {
51+
if !strings.HasPrefix(token, header) {
52+
return nil, nil, ErrIncorrectTokenHeader
53+
}
54+
55+
parts := bytes.Split([]byte(token[len(header):]), []byte("."))
56+
57+
var rawPayload, rawFooter []byte
58+
switch len(parts) {
59+
case 1:
60+
rawPayload = parts[0]
61+
case 2:
62+
rawPayload = parts[0]
63+
rawFooter = parts[1]
64+
default:
65+
return nil, nil, ErrIncorrectTokenFormat
66+
}
67+
68+
payload := make([]byte, b64DecodedLen(len(rawPayload)))
69+
if _, err := b64Decode(payload, rawPayload); err != nil {
70+
return nil, nil, fmt.Errorf("decode payload: %w", err)
71+
}
72+
73+
var footer []byte
74+
if rawFooter != nil {
75+
footer = make([]byte, b64DecodedLen(len(rawFooter)))
76+
if _, err := b64Decode(footer, rawFooter); err != nil {
77+
return nil, nil, fmt.Errorf("decode footer: %w", err)
78+
}
79+
}
80+
return payload, footer, nil
81+
}
82+
83+
func buildToken(header string, body, footer []byte) string {
84+
size := len(header) + b64EncodedLen(len(body))
85+
if len(footer) > 0 {
86+
size += 1 + b64EncodedLen(len(footer))
87+
}
88+
89+
token := make([]byte, size)
90+
offset := 0
91+
offset += copy(token[offset:], header)
92+
93+
b64Encode(token[offset:], body)
94+
offset += b64EncodedLen(len(body))
95+
96+
if len(footer) > 0 {
97+
offset += copy(token[offset:], ".")
98+
b64Encode(token[offset:], footer)
99+
}
100+
return string(token)
101+
}
102+
103+
func b64Decode(dst, src []byte) (n int, err error) {
104+
return base64.RawURLEncoding.Decode(dst, src)
105+
}
106+
107+
func b64DecodedLen(n int) int {
108+
return base64.RawURLEncoding.DecodedLen(n)
109+
}
110+
111+
func b64Encode(dst, src []byte) {
112+
base64.RawURLEncoding.Encode(dst, src)
113+
}
114+
115+
func b64EncodedLen(n int) int {
116+
return base64.RawURLEncoding.EncodedLen(n)
117+
}

errors.go

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package paseto
2+
3+
import "errors"
4+
5+
var (
6+
ErrDataUnmarshal = errors.New("can't unmarshal token data to the given type of value")
7+
ErrInvalidTokenAuth = errors.New("invalid token authentication")
8+
ErrIncorrectTokenFormat = errors.New("incorrect token format")
9+
ErrIncorrectTokenHeader = errors.New("incorrect token header")
10+
)

go.mod

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
module paseto
1+
module github.com/cristalhq/paseto
22

33
go 1.21
4+
5+
require golang.org/x/crypto v0.22.0

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
2+
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=

paseto_test.go

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package paseto
2+
3+
import (
4+
"encoding/hex"
5+
"encoding/json"
6+
"os"
7+
"reflect"
8+
"testing"
9+
)
10+
11+
type GoldenCases struct {
12+
Tests []GoldenCase `json:"tests"`
13+
}
14+
15+
type GoldenCase struct {
16+
Name string `json:"name"`
17+
ExpectFail bool `json:"expect-fail"`
18+
Nonce string `json:"nonce"`
19+
Key string `json:"key"`
20+
PublicKey string `json:"public-key"`
21+
SecretKey string `json:"secret-key"`
22+
SecretKeySeed string `json:"secret-key-seed"`
23+
SecretKeyPem string `json:"secret-key-pem"`
24+
PublicKeyPem string `json:"public-key-pem"`
25+
Token string `json:"token"`
26+
Payload string `json:"payload"`
27+
Footer string `json:"footer"`
28+
ImplicitAssertion string `json:"implicit-assertion"`
29+
}
30+
31+
func loadGoldenFile(filename string) GoldenCases {
32+
f, err := os.Open(filename)
33+
if err != nil {
34+
panic(err)
35+
}
36+
defer f.Close()
37+
38+
var tc GoldenCases
39+
if err := json.NewDecoder(f).Decode(&tc); err != nil {
40+
panic(err)
41+
}
42+
return tc
43+
}
44+
45+
func must[T any](v T, err error) T {
46+
if err != nil {
47+
panic(err)
48+
}
49+
return v
50+
}
51+
52+
func mustHex(raw string) []byte {
53+
return must(hex.DecodeString(raw))
54+
}
55+
56+
func mustJSON(raw string) any {
57+
if len(raw) == 0 || string(raw) == "" {
58+
return nil
59+
}
60+
var dst any
61+
if err := json.Unmarshal([]byte(raw), &dst); err != nil {
62+
return string(raw)
63+
}
64+
return dst
65+
}
66+
67+
func mustOk(tb testing.TB, err error) {
68+
tb.Helper()
69+
if err != nil {
70+
tb.Fatal(err)
71+
}
72+
}
73+
74+
func mustFail(tb testing.TB, err error) {
75+
tb.Helper()
76+
if err == nil {
77+
tb.Fatal()
78+
}
79+
}
80+
81+
func mustEqual[T any](tb testing.TB, have, want T) {
82+
tb.Helper()
83+
if !reflect.DeepEqual(have, want) {
84+
tb.Fatalf("\nhave: %+v\nwant: %+v\n", have, want)
85+
}
86+
}

testdata/v1.json

+145
Large diffs are not rendered by default.

v1loc.go

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package paseto
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"crypto/hmac"
7+
"crypto/rand"
8+
"crypto/sha512"
9+
"fmt"
10+
"io"
11+
12+
"golang.org/x/crypto/hkdf"
13+
)
14+
15+
const (
16+
v1LocNonceSize = 32
17+
v1LocNonceHalf = v1LocNonceSize / 2
18+
v1LocMacSize = 48 // const for crypty.SHA384.Size()
19+
v1LocHeader = "v1.local."
20+
)
21+
22+
func V1Encrypt(key []byte, payload, footer any, randBytes []byte) (string, error) {
23+
if randBytes == nil {
24+
randBytes = make([]byte, v1LocNonceSize)
25+
if _, err := io.ReadFull(rand.Reader, randBytes); err != nil {
26+
return "", fmt.Errorf("read from crypto/rand.Reader: %w", err)
27+
}
28+
}
29+
30+
payloadBytes, err := toBytes(payload)
31+
if err != nil {
32+
return "", fmt.Errorf("encode payload: %w", err)
33+
}
34+
35+
footerBytes, err := toBytes(footer)
36+
if err != nil {
37+
return "", fmt.Errorf("encode footer: %w", err)
38+
}
39+
40+
macN := hmac.New(sha512.New384, randBytes)
41+
if _, err := macN.Write(payloadBytes); err != nil {
42+
return "", fmt.Errorf("hash payload: %w", err)
43+
}
44+
nonce := macN.Sum(nil)[:v1LocNonceSize]
45+
46+
encKey, authKey, err := v1locSplitKey(key, nonce[:v1LocNonceHalf])
47+
if err != nil {
48+
return "", fmt.Errorf("create enc and auth keys: %w", err)
49+
}
50+
51+
block, err := aes.NewCipher(encKey)
52+
if err != nil {
53+
return "", fmt.Errorf("create aes cipher: %w", err)
54+
}
55+
56+
encryptedPayload := make([]byte, len(payloadBytes))
57+
cipher.NewCTR(block, nonce[v1LocNonceHalf:]).
58+
XORKeyStream(encryptedPayload, payloadBytes)
59+
60+
h := hmac.New(sha512.New384, authKey)
61+
if _, err := h.Write(pae([]byte(v1LocHeader), nonce, encryptedPayload, footerBytes)); err != nil {
62+
return "", fmt.Errorf("create signature: %w", err)
63+
}
64+
mac := h.Sum(nil)
65+
66+
body := make([]byte, 0, len(nonce)+len(encryptedPayload)+len(mac))
67+
body = append(body, nonce...)
68+
body = append(body, encryptedPayload...)
69+
body = append(body, mac...)
70+
71+
return buildToken(v1LocHeader, body, footerBytes), nil
72+
}
73+
74+
func V1Decrypt(token string, key []byte, payload, footer any) error {
75+
data, footerBytes, err := splitToken(token, v1LocHeader)
76+
if err != nil {
77+
return fmt.Errorf("decode token: %w", err)
78+
}
79+
if len(data) < v1LocNonceSize+v1LocMacSize {
80+
return ErrIncorrectTokenFormat
81+
}
82+
83+
pivot := len(data) - v1LocMacSize
84+
nonce := data[:v1LocNonceSize]
85+
encryptedPayload, mac := data[v1LocNonceSize:pivot], data[pivot:]
86+
87+
encKey, authKey, err := v1locSplitKey(key, nonce[:v1LocNonceHalf])
88+
if err != nil {
89+
return fmt.Errorf("create enc and auth keys: %w", err)
90+
}
91+
92+
body := pae([]byte(v1LocHeader), nonce, encryptedPayload, footerBytes)
93+
h := hmac.New(sha512.New384, authKey)
94+
if _, err := h.Write(body); err != nil {
95+
return fmt.Errorf("create signature: %w", err)
96+
}
97+
98+
if !hmac.Equal(h.Sum(nil), mac) {
99+
return fmt.Errorf("token signature: %w", ErrInvalidTokenAuth)
100+
}
101+
102+
block, err := aes.NewCipher(encKey)
103+
if err != nil {
104+
return fmt.Errorf("create aes cipher: %w", err)
105+
}
106+
107+
decryptedPayload := make([]byte, len(encryptedPayload))
108+
cipher.NewCTR(block, nonce[v1LocNonceHalf:]).
109+
XORKeyStream(decryptedPayload, encryptedPayload)
110+
111+
if payload != nil {
112+
if err := fromBytes(decryptedPayload, payload); err != nil {
113+
return fmt.Errorf("decode payload: %w", err)
114+
}
115+
}
116+
117+
if footer != nil {
118+
if err := fromBytes(footerBytes, footer); err != nil {
119+
return fmt.Errorf("decode footer: %w", err)
120+
}
121+
}
122+
return nil
123+
}
124+
125+
func v1locSplitKey(key, salt []byte) ([]byte, []byte, error) {
126+
eReader := hkdf.New(sha512.New384, key, salt, []byte("paseto-encryption-key"))
127+
aReader := hkdf.New(sha512.New384, key, salt, []byte("paseto-auth-key-for-aead"))
128+
129+
encKey := make([]byte, 32)
130+
authKey := make([]byte, 32)
131+
132+
if _, err := io.ReadFull(eReader, encKey); err != nil {
133+
return nil, nil, err
134+
}
135+
if _, err := io.ReadFull(aReader, authKey); err != nil {
136+
return nil, nil, err
137+
}
138+
return encKey, authKey, nil
139+
}

0 commit comments

Comments
 (0)