Skip to content

Commit 59de68a

Browse files
zepatrikory-bot
authored andcommitted
fix: meaningful error and panic in pagination tokens
GitOrigin-RevId: 5e7fa89b3380c4052c8d7f9f850abf375564d2ed
1 parent 5d309de commit 59de68a

3 files changed

Lines changed: 68 additions & 12 deletions

File tree

pagination/keysetpagination_v2/page_token.go

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@
44
package keysetpagination
55

66
import (
7+
"crypto/rand"
78
"database/sql"
9+
"encoding/base64"
810
"encoding/json"
11+
"io"
912
"time"
1013

1114
"github.com/gofrs/uuid"
12-
"github.com/pkg/errors"
13-
"github.com/ssoready/hyrumtoken"
14-
1515
"github.com/ory/herodot"
16+
"github.com/pkg/errors"
17+
"golang.org/x/crypto/nacl/secretbox"
1618
)
1719

1820
var fallbackEncryptionKey = &[32]byte{}
@@ -58,7 +60,16 @@ func (t PageToken) Encrypt(keys [][32]byte) string {
5860
if len(keys) > 0 {
5961
key = &keys[0]
6062
}
61-
return hyrumtoken.Marshal(key, t)
63+
enc, err := t.encrypt(key)
64+
if err != nil {
65+
// This should basically never happen, only if reading from the random source or marshaling the token fails.
66+
// In both cases, we have a bigger problem than just not being able to generate the page token.
67+
// Therefore, if we do get an error, there is no point in returning it to the client,
68+
// as we already have a working result set. With this string, the next page will return an error,
69+
// but that is better than breaking the current page.
70+
return "internal error: failed to generate page token"
71+
}
72+
return enc
6273
}
6374

6475
func (t PageToken) MarshalJSON() ([]byte, error) {
@@ -109,7 +120,7 @@ func (t PageToken) MarshalJSON() ([]byte, error) {
109120
return json.Marshal(toEncode)
110121
}
111122

112-
var ErrPageTokenExpired = herodot.ErrBadRequest.WithReason("page token expired, do not persist page tokens")
123+
var ErrPageTokenExpired = herodot.ErrBadRequest.WithError("page token expired, do not persist page tokens")
113124

114125
func (t *PageToken) UnmarshalJSON(data []byte) error {
115126
rawToken := jsonPageToken{}
@@ -149,3 +160,46 @@ func (t *PageToken) UnmarshalJSON(data []byte) error {
149160
}
150161

151162
func NewPageToken(cols ...Column) PageToken { return PageToken{cols: cols} }
163+
164+
func (t *PageToken) encrypt(key *[32]byte) (string, error) {
165+
var nonce [24]byte
166+
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
167+
return "", errors.Wrap(err, "cannot seed nonce")
168+
}
169+
170+
raw, err := json.Marshal(t)
171+
if err != nil {
172+
return "", errors.Wrap(err, "cannot marshal page token")
173+
}
174+
175+
enc := secretbox.Seal(nonce[:], raw, &nonce, key)
176+
return base64.URLEncoding.EncodeToString(enc), nil
177+
}
178+
179+
func (t *PageToken) decrypt(key *[32]byte, s string) error {
180+
if s == "" {
181+
return errors.WithStack(ErrInvalidPaginationToken)
182+
}
183+
184+
raw, err := base64.URLEncoding.DecodeString(s)
185+
if err != nil || len(raw) < 24 {
186+
return errors.WithStack(ErrInvalidPaginationToken)
187+
}
188+
189+
var nonce [24]byte
190+
copy(nonce[:], raw[:24])
191+
192+
dec, ok := secretbox.Open(nil, raw[24:], &nonce, key)
193+
if !ok {
194+
return errors.WithStack(ErrInvalidPaginationToken)
195+
}
196+
197+
if err := json.Unmarshal(dec, t); err != nil {
198+
if errors.As(err, new(*herodot.DefaultError)) {
199+
return err
200+
}
201+
return errors.WithStack(herodot.ErrInternalServerError.WithReason("unable to unmarshal page token").WithDebug(err.Error()))
202+
}
203+
204+
return nil
205+
}

pagination/keysetpagination_v2/paginator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"github.com/ory/herodot"
1313
)
1414

15-
var ErrInvalidPaginationToken = herodot.ErrBadRequest.WithReason("invalid pagination token")
15+
var ErrInvalidPaginationToken = herodot.ErrBadRequest.WithError("invalid pagination token")
1616

1717
type (
1818
Paginator struct {

pagination/keysetpagination_v2/request_params.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"strings"
1313

1414
"github.com/pkg/errors"
15-
"github.com/ssoready/hyrumtoken"
1615
)
1716

1817
// Pagination Request Parameters
@@ -110,15 +109,18 @@ func ParseQueryParams(keys [][32]byte, q url.Values) ([]Option, error) {
110109
}
111110

112111
// ParsePageToken parses a page token from the given raw string using the provided keys.
113-
// It panics if no keys are provided.
112+
// If decryption fails with all provided keys (including when no keys are given), it
113+
// falls back to using the fallbackEncryptionKey and returns an error if that also fails.
114114
func ParsePageToken(keys [][32]byte, raw string) (t PageToken, err error) {
115115
for i := range keys {
116-
err = errors.WithStack(hyrumtoken.Unmarshal(&keys[i], raw, &t))
117-
if err == nil {
118-
return
116+
err = errors.WithStack(t.decrypt(&keys[i], raw))
117+
if errors.Is(err, ErrInvalidPaginationToken) {
118+
continue
119119
}
120+
// either we successfully decrypted the token, or we got an error that is not ErrInvalidPaginationToken, in both cases we should return immediately
121+
return t, err
120122
}
121123
// as a last resort, try the fallback key
122-
err = hyrumtoken.Unmarshal(fallbackEncryptionKey, raw, &t)
124+
err = t.decrypt(fallbackEncryptionKey, raw)
123125
return t, errors.WithStack(err)
124126
}

0 commit comments

Comments
 (0)