hex.go raw
1 package validation
2
3 import (
4 "bytes"
5 "fmt"
6 )
7
8 // ValidateLowercaseHexInJSON checks that all hex-encoded fields in the raw JSON are lowercase.
9 // NIP-01 specifies that hex encoding must be lowercase.
10 // This must be called on the raw message BEFORE unmarshaling, since unmarshal converts
11 // hex strings to binary and loses case information.
12 // Returns an error message if validation fails, or empty string if valid.
13 func ValidateLowercaseHexInJSON(msg []byte) string {
14 // Find and validate "id" field (64 hex chars)
15 if err := validateJSONHexField(msg, `"id"`); err != "" {
16 return err + " (id)"
17 }
18
19 // Find and validate "pubkey" field (64 hex chars)
20 if err := validateJSONHexField(msg, `"pubkey"`); err != "" {
21 return err + " (pubkey)"
22 }
23
24 // Find and validate "sig" field (128 hex chars)
25 if err := validateJSONHexField(msg, `"sig"`); err != "" {
26 return err + " (sig)"
27 }
28
29 // Validate e and p tags in the tags array
30 // Tags format: ["e", "hexvalue", ...] or ["p", "hexvalue", ...]
31 if err := validateEPTagsInJSON(msg); err != "" {
32 return err
33 }
34
35 return "" // Valid
36 }
37
38 // validateJSONHexField finds a JSON field and checks if its hex value contains uppercase.
39 func validateJSONHexField(msg []byte, fieldName string) string {
40 // Find the field name
41 idx := bytes.Index(msg, []byte(fieldName))
42 if idx == -1 {
43 return "" // Field not found, skip
44 }
45
46 // Find the colon after the field name
47 colonIdx := bytes.Index(msg[idx:], []byte(":"))
48 if colonIdx == -1 {
49 return ""
50 }
51
52 // Find the opening quote of the value
53 valueStart := idx + colonIdx + 1
54 for valueStart < len(msg) && (msg[valueStart] == ' ' || msg[valueStart] == '\t' || msg[valueStart] == '\n' || msg[valueStart] == '\r') {
55 valueStart++
56 }
57 if valueStart >= len(msg) || msg[valueStart] != '"' {
58 return ""
59 }
60 valueStart++ // Skip the opening quote
61
62 // Find the closing quote
63 valueEnd := valueStart
64 for valueEnd < len(msg) && msg[valueEnd] != '"' {
65 valueEnd++
66 }
67
68 // Extract the hex value and check for uppercase
69 hexValue := msg[valueStart:valueEnd]
70 if containsUppercaseHex(hexValue) {
71 return "blocked: hex fields may only be lower case, see NIP-01"
72 }
73
74 return ""
75 }
76
77 // validateEPTagsInJSON checks e and p tags in the JSON for uppercase hex.
78 func validateEPTagsInJSON(msg []byte) string {
79 // Find the tags array
80 tagsIdx := bytes.Index(msg, []byte(`"tags"`))
81 if tagsIdx == -1 {
82 return "" // No tags
83 }
84
85 // Find the opening bracket of the tags array
86 bracketIdx := bytes.Index(msg[tagsIdx:], []byte("["))
87 if bracketIdx == -1 {
88 return ""
89 }
90
91 tagsStart := tagsIdx + bracketIdx
92
93 // Scan through to find ["e", ...] and ["p", ...] patterns
94 // This is a simplified parser that looks for specific patterns
95 pos := tagsStart
96 for pos < len(msg) {
97 // Look for ["e" or ["p" pattern
98 eTagPattern := bytes.Index(msg[pos:], []byte(`["e"`))
99 pTagPattern := bytes.Index(msg[pos:], []byte(`["p"`))
100
101 var tagType string
102 var nextIdx int
103
104 if eTagPattern == -1 && pTagPattern == -1 {
105 break // No more e or p tags
106 } else if eTagPattern == -1 {
107 nextIdx = pos + pTagPattern
108 tagType = "p"
109 } else if pTagPattern == -1 {
110 nextIdx = pos + eTagPattern
111 tagType = "e"
112 } else if eTagPattern < pTagPattern {
113 nextIdx = pos + eTagPattern
114 tagType = "e"
115 } else {
116 nextIdx = pos + pTagPattern
117 tagType = "p"
118 }
119
120 // Find the hex value after the tag type
121 // Pattern: ["e", "hexvalue" or ["p", "hexvalue"
122 commaIdx := bytes.Index(msg[nextIdx:], []byte(","))
123 if commaIdx == -1 {
124 pos = nextIdx + 4
125 continue
126 }
127
128 // Find the opening quote of the hex value
129 valueStart := nextIdx + commaIdx + 1
130 for valueStart < len(msg) && (msg[valueStart] == ' ' || msg[valueStart] == '\t' || msg[valueStart] == '"') {
131 if msg[valueStart] == '"' {
132 valueStart++
133 break
134 }
135 valueStart++
136 }
137
138 // Find the closing quote
139 valueEnd := valueStart
140 for valueEnd < len(msg) && msg[valueEnd] != '"' {
141 valueEnd++
142 }
143
144 // Check if this looks like a hex value (64 chars for pubkey/event ID)
145 hexValue := msg[valueStart:valueEnd]
146 if len(hexValue) == 64 && containsUppercaseHex(hexValue) {
147 return fmt.Sprintf("blocked: hex fields may only be lower case, see NIP-01 (%s tag)", tagType)
148 }
149
150 pos = valueEnd + 1
151 }
152
153 return ""
154 }
155
156 // containsUppercaseHex checks if a byte slice (representing hex) contains uppercase letters A-F.
157 func containsUppercaseHex(b []byte) bool {
158 for _, c := range b {
159 if c >= 'A' && c <= 'F' {
160 return true
161 }
162 }
163 return false
164 }
165