-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
287 lines (236 loc) · 8.79 KB
/
main.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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
package strkey
import (
"encoding/base32"
"encoding/binary"
"fmt"
"github.com/shantanu-hashcash/go/strkey/internal/crc16"
"github.com/shantanu-hashcash/go/support/errors"
)
// ErrInvalidVersionByte is returned when the version byte from a provided
// strkey-encoded string is not one of the valid values.
var ErrInvalidVersionByte = errors.New("invalid version byte")
// VersionByte represents one of the possible prefix values for a StrKey base
// string--the string the when encoded using base32 yields a final StrKey.
type VersionByte byte
const (
//VersionByteAccountID is the version byte used for encoded hcnet addresses
VersionByteAccountID VersionByte = 6 << 3 // Base32-encodes to 'G...'
//VersionByteSeed is the version byte used for encoded hcnet seed
VersionByteSeed = 18 << 3 // Base32-encodes to 'S...'
//VersionByteMuxedAccount is the version byte used for encoded hcnet multiplexed addresses
VersionByteMuxedAccount = 12 << 3 // Base32-encodes to 'M...'
//VersionByteHashTx is the version byte used for encoded hcnet hashTx
//signer keys.
VersionByteHashTx = 19 << 3 // Base32-encodes to 'T...'
//VersionByteHashX is the version byte used for encoded hcnet hashX
//signer keys.
VersionByteHashX = 23 << 3 // Base32-encodes to 'X...'
//VersionByteSignedPayload is the version byte used for encoding "signed
//payload" (CAP-40) signer keys.
VersionByteSignedPayload = 15 << 3 // Base-32 encodes to 'P'
//VersionByteContract is the version byte used for encoded hcnet contracts
VersionByteContract = 2 << 3 // Base-32 encodes to 'C'
)
// maxPayloadSize is the maximum length of the payload for all versions. The
// largest strkey is a signed payload: 32-byte public key + 4-byte payload
// length + 64-byte payload
const maxPayloadSize = 100
// maxRawSize is the maximum length of a strkey in its raw form not encoded.
const maxRawSize = 1 + /* version byte */ maxPayloadSize + 2 /* checksum */
// maxEncodedSize is the maximum length of a strkey when base32 encoded.
const maxEncodedSize = (maxRawSize*8 + 4) / 5 // (8n+4)/5 is the EncodedLen for no padding
// encoding to use when encoding and decoding a strkey to and from strings.
var encoding = base32.StdEncoding.WithPadding(base32.NoPadding)
// DecodeAny decodes the provided StrKey into a raw value, checking the checksum
// and if the version byte is one of allowed values.
func DecodeAny(src string) (VersionByte, []byte, error) {
raw, err := decodeString(src)
if err != nil {
return 0, nil, err
}
// decode into components
version := VersionByte(raw[0])
vp := raw[0 : len(raw)-2]
payload := raw[1 : len(raw)-2]
checksum := raw[len(raw)-2:]
// ensure version byte is allowed
if err := checkValidVersionByte(version); err != nil {
return 0, nil, err
}
// ensure checksum is valid
if err := crc16.Validate(vp, binary.LittleEndian.Uint16(checksum)); err != nil {
return 0, nil, err
}
// if we made it through the gaunlet, return the decoded value
return version, payload, nil
}
// Decode decodes the provided StrKey into a raw value, checking the checksum
// and ensuring the expected VersionByte (the version parameter) is the value
// actually encoded into the provided src string.
func Decode(expected VersionByte, src string) ([]byte, error) {
if err := checkValidVersionByte(expected); err != nil {
return nil, err
}
raw, err := decodeString(src)
if err != nil {
return nil, err
}
// check length
if len(raw) < 3 {
return nil, errors.New("decoded string is too short")
}
// decode into components
version := VersionByte(raw[0])
vp := raw[0 : len(raw)-2]
payload := raw[1 : len(raw)-2]
checksum := raw[len(raw)-2:]
// ensure version byte is expected
if version != expected {
return nil, ErrInvalidVersionByte
}
// ensure checksum is valid
if err := crc16.Validate(vp, binary.LittleEndian.Uint16(checksum)); err != nil {
return nil, err
}
// if we made it through the gauntlet, return the decoded value
return payload, nil
}
// MustDecode is like Decode, but panics on error
func MustDecode(expected VersionByte, src string) []byte {
d, err := Decode(expected, src)
if err != nil {
panic(err)
}
return d
}
// Encode encodes the provided data to a StrKey, using the provided version
// byte.
func Encode(version VersionByte, src []byte) (string, error) {
if err := checkValidVersionByte(version); err != nil {
return "", err
}
payloadSize := len(src)
// check src does not exceed maximum payload size
if payloadSize > maxPayloadSize {
return "", fmt.Errorf("data exceeds maximum payload size for strkey")
}
// pack
// 1 byte version
// src bytes
// 2 byte crc16
rawArr := [maxRawSize]byte{}
rawSize := 1 + payloadSize + 2
raw := rawArr[:rawSize]
raw[0] = byte(version)
copy(raw[1:], src)
crc := crc16.Checksum(raw[:1+payloadSize])
binary.LittleEndian.PutUint16(raw[1+payloadSize:], crc)
// base32 encode
encArr := [maxEncodedSize]byte{}
encSize := encoding.EncodedLen(rawSize)
enc := encArr[:encSize]
encoding.Encode(enc, raw)
return string(enc), nil
}
// MustEncode is like Encode, but panics on error
func MustEncode(version VersionByte, src []byte) string {
e, err := Encode(version, src)
if err != nil {
panic(err)
}
return e
}
// Version extracts and returns the version byte from the provided source
// string.
func Version(src string) (VersionByte, error) {
raw, err := decodeString(src)
if err != nil {
return VersionByte(0), err
}
return VersionByte(raw[0]), nil
}
// checkValidVersionByte returns an error if the provided value
// is not one of the defined valid version byte constants.
func checkValidVersionByte(version VersionByte) error {
switch version {
case VersionByteAccountID, VersionByteMuxedAccount, VersionByteSeed, VersionByteHashTx, VersionByteHashX, VersionByteSignedPayload, VersionByteContract:
return nil
default:
return ErrInvalidVersionByte
}
}
var decodingTable = initDecodingTable()
func initDecodingTable() [256]byte {
var localDecodingTable [256]byte
for i := range localDecodingTable {
localDecodingTable[i] = 0xff
}
for i, ch := range []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567") {
localDecodingTable[ch] = byte(i)
}
return localDecodingTable
}
// decodeString decodes a base32 string into the raw bytes, and ensures it could
// potentially be strkey encoded (i.e. it has both a version byte and a
// checksum, neither of which are explicitly checked by this func)
func decodeString(src string) ([]byte, error) {
// operations on strings are expensive since it involves unicode parsing
// so, we use bytes from the beginning
srcBytes := []byte(src)
// The minimal binary decoded length is 3 bytes (version byte and 2-byte CRC) which,
// in unpadded base32 (since each character provides 5 bits) corresponds to ceiling(8*3/5) = 5
if len(srcBytes) < 5 {
return nil, errors.Errorf("strkey is %d bytes long; minimum valid length is 5", len(srcBytes))
}
// SEP23 enforces strkeys to be in canonical base32 representation.
// Go's decoder doesn't help us there, so we need to do it ourselves.
// 1. Make sure there is no full unused leftover byte at the end
// (i.e. there shouldn't be 5 or more leftover bits)
leftoverBits := (len(srcBytes) * 5) % 8
if leftoverBits >= 5 {
return nil, errors.New("non-canonical strkey; unused leftover character")
}
// 2. In the last byte of the strkey there may be leftover bits (4 at most, otherwise it would be a full byte,
// which we have for checked above). If there are any leftover bits, they should be set to 0
if leftoverBits > 0 {
lastChar := srcBytes[len(srcBytes)-1]
decodedLastChar := decodingTable[lastChar]
if decodedLastChar == 0xff {
// The last character from the input wasn't in the expected input alphabet.
// Let's output an error matching the errors from the base32 decoder invocation below
return nil, errors.Wrap(base32.CorruptInputError(len(srcBytes)), "base32 decode failed")
}
leftoverBitsMask := byte(0x0f) >> (4 - leftoverBits)
if decodedLastChar&leftoverBitsMask != 0 {
return nil, errors.New("non-canonical strkey; unused bits should be set to 0")
}
}
n, err := base32.StdEncoding.WithPadding(base32.NoPadding).Decode(srcBytes, srcBytes)
if err != nil {
return nil, errors.Wrap(err, "base32 decode failed")
}
return srcBytes[:n], nil
}
// IsValidEd25519PublicKey validates a hcnet public key
func IsValidEd25519PublicKey(i interface{}) bool {
enc, ok := i.(string)
if !ok {
return false
}
_, err := Decode(VersionByteAccountID, enc)
return err == nil
}
// IsValidMuxedAccountEd25519PublicKey validates a Hcnet SEP-23 muxed address.
func IsValidMuxedAccountEd25519PublicKey(s string) bool {
_, err := Decode(VersionByteMuxedAccount, s)
return err == nil
}
// IsValidEd25519SecretSeed validates a hcnet secret key
func IsValidEd25519SecretSeed(i interface{}) bool {
enc, ok := i.(string)
if !ok {
return false
}
_, err := Decode(VersionByteSeed, enc)
return err == nil
}