/
tagged_base64.go
128 lines (110 loc) · 2.59 KB
/
tagged_base64.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
package tagged_base64
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"unicode"
"github.com/sigurn/crc8"
)
var (
BASE64 *base64.Encoding = base64.RawURLEncoding
)
type TaggedBase64 struct {
tag string
value []byte
checksum byte
}
func New(tag string, value []byte) (*TaggedBase64, error) {
if err := checkSafeBase64Tag(tag); err != nil {
return nil, err
}
return &TaggedBase64{
tag: tag,
value: value,
checksum: calcChecksum(tag, value),
}, nil
}
func (t *TaggedBase64) String() string {
data := append(t.value, t.checksum)
return fmt.Sprintf("%s~%s", t.tag, BASE64.EncodeToString(data))
}
func Parse(s string) (*TaggedBase64, error) {
// Split tag and data.
tokens := strings.Split(s, "~")
if len(tokens) < 2 {
return nil, fmt.Errorf("missing delimeter")
} else if len(tokens) > 2 {
return nil, fmt.Errorf("too many delimiters")
}
tag := tokens[0]
base64 := tokens[1]
if err := checkSafeBase64Tag(tag); err != nil {
return nil, err
}
// Decode data from base64.
data, err := BASE64.DecodeString(base64)
if err != nil {
return nil, fmt.Errorf("invalid base 64: %w", err)
}
// Verify checksum.
if len(data) == 0 {
return nil, fmt.Errorf("missing checksum")
}
value := data[:len(data)-1]
cs := data[len(data)-1]
if cs != calcChecksum(tag, value) {
return nil, fmt.Errorf("incorrect checksum")
}
return &TaggedBase64{
tag: tag,
value: value,
checksum: cs,
}, nil
}
func (t *TaggedBase64) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}
func (t *TaggedBase64) UnmarshalJSON(in []byte) error {
var s string
if err := json.Unmarshal(in, &s); err != nil {
return err
}
parsed, err := Parse(s)
if err != nil {
return err
}
*t = *parsed
return nil
}
func (t *TaggedBase64) Tag() string {
return t.tag
}
func (t *TaggedBase64) Value() []byte {
return t.value
}
func checkSafeBase64Tag(tag string) error {
for _, c := range tag {
if err := checkSafeBase64Ascii(c); err != nil {
return fmt.Errorf("invalid tag %s: %w", tag, err)
}
}
return nil
}
func checkSafeBase64Ascii(c rune) error {
if c > unicode.MaxASCII {
return fmt.Errorf("non-ASCII character %c", c)
}
// Allow alphanumeric characters, plus - and _
if !(unicode.IsLetter(c) || unicode.IsDigit(c) || c == '-' || c == '_') {
return fmt.Errorf("disallowed character %c", c)
}
return nil
}
func calcChecksum(tag string, value []byte) byte {
table := crc8.MakeTable(crc8.CRC8)
cs := crc8.Init(table)
cs = crc8.Update(cs, []byte(tag), table)
cs = crc8.Update(cs, value, table)
return byte(crc8.Complete(cs, table)) ^ byte(len(value))
}