Repository navigation
Expand file tree
/
Copy pathpixbrcode.go
More file actions
217 lines (185 loc) · 5.81 KB
/
Copy pathpixbrcode.go
File metadata and controls
217 lines (185 loc) · 5.81 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
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
package brdoc
import (
"fmt"
"regexp"
"strconv"
"strings"
)
const pixGUI = "br.gov.bcb.pix"
var (
pixMCCRegexp = regexp.MustCompile(`^\d{4}$`)
pixTxIDRegexp = regexp.MustCompile(`^([0-9A-Za-z]{1,25}|\*\*\*)$`)
pixISPBRegexp = regexp.MustCompile(`^[0-9A-Za-z]{8}$`)
// Not official logic: the format of the amount, which is only shown by
// examples.
pixAmountRegexp = regexp.MustCompile(`^\d{1,10}(\.\d{1,2})?$`)
)
// emvObject is a data object of an EMV QR Code: an ID and its value.
type emvObject struct {
id int
value string
}
// IsPixBRCode verifies if the given string is a valid BR Code of Pix, the
// content of its QR Code, also used as "Pix copia e cola". It can be static
// (with a Pix key), dynamic (with a URL) or composite (with a URL of the
// recurrence parameters).
//
// The structure, the mandatory fields and the CRC are defined by the
// [Manual do BR Code], which follows the EMV QR Code standard, and the fields
// of Pix by section 2 of the [Manual de Padrões para Iniciação do Pix]. The Pix
// key is validated by [IsPixKey].
//
// [Manual do BR Code]: https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf
// [Manual de Padrões para Iniciação do Pix]: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf
func IsPixBRCode(doc string) bool {
objects, ok := parseEMVObjects(doc)
if !ok || len(objects) < 2 {
return false
}
// The payload format indicator is the first object, and the CRC the last
// one, calculated over everything before its value.
first, last := objects[0], objects[len(objects)-1]
if first.id != 0 ||
first.value != "01" ||
last.id != 63 ||
len(last.value) != 4 ||
!strings.EqualFold(last.value, calcEMVCRC(doc[:len(doc)-4])) {
return false
}
root := map[int]string{}
for _, o := range objects {
root[o.id] = o.value
}
if v, ok := root[1]; ok && v != "11" && v != "12" {
return false
}
if v, ok := root[54]; ok && !pixAmountRegexp.MatchString(v) {
return false
}
if !pixMCCRegexp.MatchString(root[52]) || root[53] != "986" ||
root[58] != "BR" || !validEMVLength(root[59], 25) ||
!validEMVLength(root[60], 15) {
return false
}
additional, ok := parseEMVTemplate(root[62])
if !ok || !pixTxIDRegexp.MatchString(additional[5]) {
return false
}
account, recurrence, ok := findPixTemplates(objects)
if !ok {
return false
}
return validPixAccount(account, recurrence)
}
// findPixTemplates returns the merchant account information (IDs 26 to 51) and
// the unreserved template (IDs 80 to 99) of Pix, the latter only if present.
// All the templates of these ranges must have a GUI.
func findPixTemplates(objects []emvObject) (account, recurrence map[int]string, ok bool) {
for _, o := range objects {
if (o.id < 26 || o.id > 51) && o.id < 80 {
continue
}
template, ok := parseEMVTemplate(o.value)
if !ok || template[0] == "" {
return nil, nil, false
}
if !strings.EqualFold(template[0], pixGUI) {
continue
}
if o.id <= 51 && account == nil {
account = template
} else if o.id >= 80 && recurrence == nil {
recurrence = template
}
}
return account, recurrence, account != nil
}
// validPixAccount verifies the merchant account information of Pix. A static BR
// Code has a Pix key, a dynamic one has a URL, and a composite one may have
// none of them, as long as it has the URL of the recurrence parameters.
func validPixAccount(account, recurrence map[int]string) bool {
key, hasKey := account[1]
url, hasURL := account[25]
switch {
case hasKey && hasURL:
return false
case hasKey && !IsPixKey(key):
return false
case hasURL && !validPixURL(url):
return false
}
if v, ok := account[2]; ok && !validEMVLength(v, 72) {
return false
}
if v, ok := account[3]; ok && !pixISPBRegexp.MatchString(v) {
return false
}
if recurrence != nil {
return validPixURL(recurrence[25])
}
return hasKey || hasURL
}
// validPixURL verifies a URL of a BR Code, which has up to 77 characters and no
// protocol prefix.
func validPixURL(url string) bool {
// Not official logic: the URL is not fully validated.
return validEMVLength(url, 77) && !strings.Contains(url, "://")
}
// validEMVLength verifies if `value` has from 1 to `limit` characters.
func validEMVLength(value string, limit int) bool {
n := len([]rune(value))
return n >= 1 && n <= limit
}
// parseEMVTemplate returns the data objects of a template by their IDs, or
// false if it is malformed or has repeated IDs.
func parseEMVTemplate(value string) (map[int]string, bool) {
objects, ok := parseEMVObjects(value)
if !ok {
return nil, false
}
template := map[int]string{}
for _, o := range objects {
template[o.id] = o.value
}
return template, true
}
// parseEMVObjects splits `doc` in its data objects, each one with an ID of 2
// digits, a length of 2 digits and a value of that many characters. It returns
// false if `doc` is malformed or has repeated IDs.
func parseEMVObjects(doc string) ([]emvObject, bool) {
r := []rune(doc)
seen := map[int]bool{}
var objects []emvObject
for i := 0; i < len(r); {
if i+4 > len(r) || !allDigit(string(r[i:i+4])) {
return nil, false
}
id, _ := strconv.Atoi(string(r[i : i+2]))
size, _ := strconv.Atoi(string(r[i+2 : i+4]))
i += 4
if size == 0 || i+size > len(r) || seen[id] {
return nil, false
}
seen[id] = true
objects = append(objects, emvObject{id, string(r[i : i+size])})
i += size
}
return objects, len(objects) > 0
}
// calcEMVCRC returns the CRC-16/CCITT-FALSE (polynomial 0x1021, initial value
// 0xFFFF) of `doc`, as 4 hexadecimal digits.
func calcEMVCRC(doc string) string {
crc := 0xFFFF
for i := 0; i < len(doc); i++ {
crc ^= int(doc[i]) << 8
for j := 0; j < 8; j++ {
if crc&0x8000 != 0 {
crc = crc<<1 ^ 0x1021
} else {
crc <<= 1
}
}
crc &= 0xFFFF
}
return fmt.Sprintf("%04X", crc)
}