Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework v1.local #9

Merged
merged 2 commits into from
Apr 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package paseto

import (
"bytes"
"crypto/subtle"
"encoding/base64"
"encoding/binary"
"encoding/json"
Expand Down Expand Up @@ -50,7 +51,7 @@ func fromBytes(data []byte, x any) error {
*f = append(*f, data...)
default:
if err := json.Unmarshal(data, x); err != nil {
return fmt.Errorf("%v: %w", err, ErrDataUnmarshal)
return fmt.Errorf("%w: %v", ErrDataUnmarshal, err)
}
}
return nil
Expand Down Expand Up @@ -89,7 +90,7 @@ func splitToken(token, header string) ([]byte, []byte, error) {
return payload, footer, nil
}

func buildToken(header string, body, footer []byte) string {
func buildToken(header, body, footer []byte) string {
size := len(header) + b64EncodedLen(len(body))
if len(footer) > 0 {
size += 1 + b64EncodedLen(len(footer))
Expand Down Expand Up @@ -124,3 +125,7 @@ func b64Encode(dst, src []byte) {
func b64EncodedLen(n int) int {
return base64.RawURLEncoding.EncodedLen(n)
}

func constTimeEq(x, y int32) bool {
return subtle.ConstantTimeEq(x, y) == 1
}
2 changes: 1 addition & 1 deletion common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func BenchmarkPAE(b *testing.B) {
var footerBytes []byte

pieces := [][]byte{
[]byte(v1LocHeader),
[]byte(v1locHeader),
nonce[:],
encryptedPayload[:],
footerBytes,
Expand Down
146 changes: 95 additions & 51 deletions v1loc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,23 @@ import (
"crypto/hmac"
"crypto/rand"
"crypto/sha512"
"errors"
"fmt"
"io"
"strings"

"golang.org/x/crypto/hkdf"
)

const (
v1LocHeader = "v1.local."
v1LocNonceSize = 32
v1LocNonceHalf = v1LocNonceSize / 2
v1LocMacSize = 48 // const for crypty.SHA384.Size()
v1locHeader = "v1.local."
v1locKey = 32
v1locNonce = 32
v1locNonceH = v1locNonce / 2
v1locMac = 48 // const for crypto.SHA384.Size()
)

func V1Encrypt(key []byte, payload, footer any, randBytes []byte) (string, error) {
if randBytes == nil {
randBytes = make([]byte, v1LocNonceSize)
if _, err := io.ReadFull(rand.Reader, randBytes); err != nil {
return "", fmt.Errorf("read from crypto/rand.Reader: %w", err)
}
}

payloadBytes, err := toBytes(payload)
if err != nil {
return "", fmt.Errorf("encode payload: %w", err)
Expand All @@ -37,76 +33,124 @@ func V1Encrypt(key []byte, payload, footer any, randBytes []byte) (string, error
return "", fmt.Errorf("encode footer: %w", err)
}

macN := hmac.New(sha512.New384, randBytes)
if _, err := macN.Write(payloadBytes); err != nil {
return "", fmt.Errorf("hash payload: %w", err)
m := payloadBytes
k := key
f := footerBytes

// step 1.
if !constTimeEq(int32(len(k)), v1locKey) {
return "", errors.New("bad key")
}
nonce := macN.Sum(nil)[:v1LocNonceSize]

encKey, authKey, err := v1locSplitKey(key, nonce[:v1LocNonceHalf])
// step 2.
h := []byte(v1locHeader)

// step 3.
b := randBytes
if b == nil {
b = make([]byte, v1locNonce)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return "", fmt.Errorf("read from crypto/rand.Reader: %w", err)
}
}

// step 4.
hash := hmac.New(sha512.New384, b)
hash.Write(m)
n := hash.Sum(nil)[:v1locNonce]

// step 5.
ek, ak, err := v1locSplitKey(k, n[:v1locNonceH])
if err != nil {
return "", fmt.Errorf("create enc and auth keys: %w", err)
}

block, err := aes.NewCipher(encKey)
// step 6.
block, err := aes.NewCipher(ek)
if err != nil {
return "", fmt.Errorf("create aes cipher: %w", err)
}
c := make([]byte, len(m))
ciph := cipher.NewCTR(block, n[v1locNonceH:])
ciph.XORKeyStream(c, m)

encryptedPayload := make([]byte, len(payloadBytes))
cipher.NewCTR(block, nonce[v1LocNonceHalf:]).
XORKeyStream(encryptedPayload, payloadBytes)
// step 7.
preAuth := pae(h, n, c, f)

h := hmac.New(sha512.New384, authKey)
if _, err := h.Write(pae([]byte(v1LocHeader), nonce, encryptedPayload, footerBytes)); err != nil {
return "", fmt.Errorf("create signature: %w", err)
}
mac := h.Sum(nil)
// step 8.
hasher := hmac.New(sha512.New384, ak)
hasher.Write(preAuth)
t := hasher.Sum(nil)

body := make([]byte, 0, len(nonce)+len(encryptedPayload)+len(mac))
body = append(body, nonce...)
body = append(body, encryptedPayload...)
body = append(body, mac...)
// step 9.
body := make([]byte, 0, len(n)+len(c)+len(t))
body = append(body, n...)
body = append(body, c...)
body = append(body, t...)

return buildToken(v1LocHeader, body, footerBytes), nil
return buildToken(h, body, f), nil
}

func V1Decrypt(token string, key []byte, payload, footer any) error {
data, footerBytes, err := splitToken(token, v1LocHeader)
// step 0.
k := key

// step 1.
if !constTimeEq(int32(len(k)), v1locKey) {
return errors.New("bad key")
}

// step 2.
// TODO: ?

// step 3.
if !strings.HasPrefix(token, v1locHeader) {
return ErrIncorrectTokenFormat
}
h := []byte(v1locHeader)

// step 4.
data, footerBytes, err := splitToken(token, v1locHeader)
if err != nil {
return fmt.Errorf("decode token: %w", err)
}
if len(data) < v1LocNonceSize+v1LocMacSize {
if len(data) < v1locNonce+v1locMac {
return ErrIncorrectTokenFormat
}
f := footerBytes

pivot := len(data) - v1LocMacSize
nonce := data[:v1LocNonceSize]
encryptedPayload, mac := data[v1LocNonceSize:pivot], data[pivot:]
pivot := len(data) - v1locMac
n := data[:v1locNonce]
c, t := data[v1locNonce:pivot], data[pivot:]

encKey, authKey, err := v1locSplitKey(key, nonce[:v1LocNonceHalf])
// step 5.
ek, ak, err := v1locSplitKey(k, n[:v1locNonceH])
if err != nil {
return fmt.Errorf("create enc and auth keys: %w", err)
}

body := pae([]byte(v1LocHeader), nonce, encryptedPayload, footerBytes)
h := hmac.New(sha512.New384, authKey)
if _, err := h.Write(body); err != nil {
return fmt.Errorf("create signature: %w", err)
}
// step 6.
preAuth := pae(h, n, c, f)

// step 7.
hasher := hmac.New(sha512.New384, ak)
hasher.Write(preAuth)
t2 := hasher.Sum(nil)

if !hmac.Equal(h.Sum(nil), mac) {
// step 8.
if !hmac.Equal(t2, t) {
return ErrInvalidTokenAuth
}

block, err := aes.NewCipher(encKey)
// step 9.
block, err := aes.NewCipher(ek)
if err != nil {
return fmt.Errorf("create aes cipher: %w", err)
}

decryptedPayload := make([]byte, len(encryptedPayload))
cipher.NewCTR(block, nonce[v1LocNonceHalf:]).
XORKeyStream(decryptedPayload, encryptedPayload)
decryptedPayload := make([]byte, len(c))
ciph := cipher.NewCTR(block, n[v1locNonceH:])
ciph.XORKeyStream(decryptedPayload, c)

if payload != nil {
if err := fromBytes(decryptedPayload, payload); err != nil {
Expand All @@ -126,14 +170,14 @@ func v1locSplitKey(key, salt []byte) ([]byte, []byte, error) {
eReader := hkdf.New(sha512.New384, key, salt, []byte("paseto-encryption-key"))
aReader := hkdf.New(sha512.New384, key, salt, []byte("paseto-auth-key-for-aead"))

encKey := make([]byte, 32)
authKey := make([]byte, 32)
ek := make([]byte, 32)
ak := make([]byte, 32)

if _, err := io.ReadFull(eReader, encKey); err != nil {
if _, err := io.ReadFull(eReader, ek); err != nil {
return nil, nil, err
}
if _, err := io.ReadFull(aReader, authKey); err != nil {
if _, err := io.ReadFull(aReader, ak); err != nil {
return nil, nil, err
}
return encKey, authKey, nil
return ek, ak, nil
}
6 changes: 3 additions & 3 deletions v1loc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ func TestV1Loc_Encrypt(t *testing.T) {
testCases := loadGoldenFile("testdata/v1.json")

for _, tc := range testCases.Tests {
if tc.Key == "" || !strings.HasPrefix(tc.Token, v1LocHeader) {
if tc.Key == "" || !strings.HasPrefix(tc.Token, v1locHeader) {
continue
}

Expand All @@ -30,8 +30,8 @@ func TestV1Loc_Encrypt(t *testing.T) {
func TestV1Loc_Decrypt(t *testing.T) {
testCases := loadGoldenFile("testdata/v1.json")

for _, tc := range testCases.Tests[:] {
if tc.Key == "" || !strings.HasPrefix(tc.Token, v1LocHeader) {
for _, tc := range testCases.Tests {
if tc.Key == "" || !strings.HasPrefix(tc.Token, v1locHeader) {
continue
}

Expand Down
2 changes: 1 addition & 1 deletion v2loc.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func V2Encrypt(key []byte, payload, footer any, randBytes []byte) (string, error
)
body := append(nonce, encryptedPayload...)

return buildToken(v2LocHeader, body, footerBytes), nil
return buildToken([]byte(v2LocHeader), body, footerBytes), nil
}

func V2Decrypt(token string, key []byte, payload, footer any) error {
Expand Down
Loading