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