-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathmigrate.go
More file actions
134 lines (111 loc) · 4 KB
/
migrate.go
File metadata and controls
134 lines (111 loc) · 4 KB
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
// Copyright 2025 Bobby Powers. All rights reserved.
// Use of this source code is governed by the MIT
// license that can be found in the LICENSE file.
package seshcookie
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"fmt"
"slices"
"strings"
"google.golang.org/protobuf/proto"
)
// MigrateFunc converts raw JSON bytes from a seshcookie-js session
// into the caller's protobuf session type. The JSON is the direct
// plaintext that was stored in the JS cookie (i.e. JSON.stringify(session)).
type MigrateFunc[T proto.Message] func(jsonData []byte) (T, error)
// migrateConfig holds pre-computed state for JS cookie migration.
type migrateConfig[T proto.Message] struct {
jsEncKey []byte
convert MigrateFunc[T]
}
// Option configures optional Handler behavior.
type Option[T proto.Message] func(*handlerOptions[T])
// handlerOptions collects all optional configuration.
type handlerOptions[T proto.Message] struct {
migrate *migrateConfig[T]
}
// WithMigration returns an Option that enables transparent migration
// from seshcookie-js cookies. jsKey is the key string that was passed
// to the JS seshcookie constructor. convert transforms the JSON session
// payload into the caller's protobuf type.
//
// When a request arrives with a JS-format cookie (no "sc1_" prefix,
// three base64 parts separated by hyphens), the handler decrypts it
// using the JS key derivation (SHA256(key)[:16]) and passes the JSON
// plaintext to convert. The resulting session is written back as a
// Go-format cookie on the response, completing the migration.
func WithMigration[T proto.Message](jsKey string, convert MigrateFunc[T]) Option[T] {
encKey := deriveJSKey(jsKey)
return func(o *handlerOptions[T]) {
o.migrate = &migrateConfig[T]{
jsEncKey: encKey,
convert: convert,
}
}
}
// deriveJSKey replicates the seshcookie-js key derivation: SHA256(key)[:16].
func deriveJSKey(key string) []byte {
h := sha256.Sum256([]byte(key))
return h[:16]
}
// decodeJSCookie decrypts a seshcookie-js cookie value and returns the
// JSON plaintext. The JS wire format is "b64(nonce)-b64(ciphertext)-b64(tag)"
// with the nonce passed as AAD.
func decodeJSCookie(encoded string, jsEncKey []byte) ([]byte, error) {
parts := strings.Split(encoded, "-")
if len(parts) != 3 {
return nil, fmt.Errorf("expected 3 parts, got %d", len(parts))
}
nonce, err := base64.StdEncoding.DecodeString(parts[0])
if err != nil {
return nil, fmt.Errorf("decode nonce: %w", err)
}
ciphertext, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("decode ciphertext: %w", err)
}
tag, err := base64.StdEncoding.DecodeString(parts[2])
if err != nil {
return nil, fmt.Errorf("decode tag: %w", err)
}
if len(nonce) != gcmNonceSize {
return nil, fmt.Errorf("nonce length %d, want %d", len(nonce), gcmNonceSize)
}
block, err := aes.NewCipher(jsEncKey)
if err != nil {
return nil, fmt.Errorf("aes.NewCipher: %w", err)
}
aeadCipher, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("cipher.NewGCM: %w", err)
}
// Go's aead.Open expects ciphertext with tag appended.
// JS separates them, so concatenate before decrypting.
// JS passes nonce as AAD via cipher.setAAD(nonce).
plaintext, err := aeadCipher.Open(nil, nonce, slices.Concat(ciphertext, tag), nonce)
if err != nil {
return nil, fmt.Errorf("aeadCipher.Open: %w", err)
}
return plaintext, nil
}
// decodeJSSession attempts to decrypt a JS-format cookie and convert
// the JSON payload to the caller's protobuf session type. Returns the
// zero value of T and an error on failure.
func (h *Handler[T]) decodeJSSession(cookieValue string) (T, error) {
var zero T
if h.opts.migrate == nil {
return zero, fmt.Errorf("no migration configured")
}
jsonData, err := decodeJSCookie(cookieValue, h.opts.migrate.jsEncKey)
if err != nil {
return zero, fmt.Errorf("decodeJSCookie: %w", err)
}
session, err := h.opts.migrate.convert(jsonData)
if err != nil {
return zero, fmt.Errorf("convert: %w", err)
}
return session, nil
}