-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmarshal.go
143 lines (114 loc) · 3.19 KB
/
marshal.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
// Copyright 2022 Teal.Finance/incorruptible contributors
// This file is part of Teal.Finance/incorruptible
// a tiny+secured cookie token licensed under the MIT License.
// SPDX-License-Identifier: MIT
package incorruptible
import (
"fmt"
"math/rand"
"github.com/klauspost/compress/s2"
)
const (
sizeMayCompress = 50 // by experience, cannot be below than 24 bytes
sizeMustCompress = 99
)
type Serializer struct {
ipLength int
nValues int // number of values
valTotalSize int // sum of the value lengths
payloadSize int // size in bytes of the uncompressed payload
compressed bool
}
func newSerializer(tv TValues) Serializer {
var s Serializer
s.ipLength = len(tv.IP) // can be 0, 4 or 16
s.nValues = len(tv.Values)
s.valTotalSize = s.nValues
for _, v := range tv.Values {
s.valTotalSize += len(v)
}
s.payloadSize = ExpirySize + s.ipLength + s.valTotalSize
s.compressed = doesCompress(s.payloadSize)
return s
}
// doesCompress decides to compress or not the payload.
// The compression decision is a bit randomized
// to limit the "chosen plaintext" attack.
//
// "math/rand" is 40 times faster than "crypto/rand"
// see: https://github.com/SimonWaldherr/golang-benchmarks#random
//
//nolint:gosec // strong random generator not required here
func doesCompress(payloadSize int) bool {
switch {
case payloadSize < sizeMayCompress:
return false
case payloadSize < sizeMustCompress:
zeroOrOne := (rand.Int63() & 1)
return (zeroOrOne == 0)
default:
return true
}
}
// Marshal serializes a TValues in a short way.
// The format starts with a magic code (2 bytes),
// followed by the expiry time, the client IP, the user-defined values,
// and ends with random salt as padding for a final size aligned on 32 bits.
func Marshal(tv TValues, magic uint8) ([]byte, error) {
s := newSerializer(tv)
b, err := s.putHeaderExpiryIP(magic, tv)
if err != nil {
return nil, err
}
b, err = s.appendValues(b, tv)
if err != nil {
return nil, err
}
if len(b) != HeaderSize+s.payloadSize {
return nil, fmt.Errorf("unexpected length got=%d want=%d", len(b), HeaderSize+s.payloadSize)
}
if s.compressed {
c := s2.Encode(nil, b[HeaderSize:])
n := copy(b[HeaderSize:], c)
if n != len(c) {
return nil, fmt.Errorf("unexpected copied bytes got=%d want=%d", n, len(c))
}
b = b[:HeaderSize+n]
}
if EnablePadding {
b = s.appendPadding(b)
}
return b, nil
}
func (s Serializer) allocateBuffer() []byte {
length := HeaderSize + ExpirySize
capacity := length + s.ipLength + s.valTotalSize
if EnablePadding {
capacity += paddingMaxSize
}
return make([]byte, length, capacity)
}
func (s Serializer) putHeaderExpiryIP(magic uint8, tv TValues) ([]byte, error) {
b := s.allocateBuffer()
m, err := NewMetadata(s.ipLength, s.compressed, s.nValues)
if err != nil {
return nil, err
}
m.PutHeader(b, magic)
err = PutExpiry(b, tv.Expires)
if err != nil {
return nil, err
}
b = AppendIP(b, tv.IP)
return b, nil
}
func (s Serializer) appendValues(buf []byte, tv TValues) ([]byte, error) {
for _, v := range tv.Values {
if len(v) > 255 {
return nil, fmt.Errorf("too large %d > 255", v)
}
buf = append(buf, uint8(len(v)))
buf = append(buf, v...)
}
return buf, nil
}