-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathudc.h
More file actions
292 lines (241 loc) · 10 KB
/
Copy pathudc.h
File metadata and controls
292 lines (241 loc) · 10 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
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
288
289
290
291
292
#ifndef UDC_H
#define UDC_H
#include <Arduino.h>
#include <ESP8266WebServer.h>
#include "env.h"
/*
GameChanger UDC / GCScript helpers.
This file owns the wallet URL format, the compact GCScript JSON builder, the
base64url codec, and the tiny parser used to recover the exported value after
GameChanger Wallet redirects back to this ESP01.
Device-friendly encoding choice:
- gzip UDC payloads are smaller, but ESP8266 Arduino does not provide a
simple native gzip encoder in the core.
- base64url is larger, but has no dependency and is deterministic on-chip.
- GameChanger uses the "0-" prefix for base64url UDC messages.
*/
// ---------- Internal UDC constants ----------
static const char UDC_WALLET_API_BASE_URL[] = "https://wallet.gamechanger.finance/api/2/run/";
static const char UDC_ENCODING[] = "base64url";
static const char UDC_BASE64URL_HEADER[] = "0-";
static const char UDC_BASE64URL_ALPHABET[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
static const char UDC_EXPORT_NAME[] = "CIP8RelayControl";
static const char UDC_INTENT_DESCRIPTION[] = "About to use a CIP-8 signature to set the state of the relay";
static const char UDC_INTENT_TITLE_ON[] = "Turn relay ON?";
static const char UDC_INTENT_TITLE_OFF[] = "Turn relay OFF?";
static const size_t UDC_INTENT_JSON_RESERVE_BYTES = 1400;
static const size_t UDC_CONNECTION_URL_RESERVE_BYTES = 2200;
static const char RELAY_STATE_ON_TEXT[] = "ON";
static const char RELAY_STATE_OFF_TEXT[] = "OFF";
static const uint8_t CHALLENGE_RANDOM_BYTES = 32;
static const uint8_t CHALLENGE_HEX_LENGTH = CHALLENGE_RANDOM_BYTES * 2;
static const uint8_t SHA512_HEX_LENGTH = 128;
static const char HEX_ALPHABET[] = "0123456789abcdef";
bool isUnreservedUrlChar(char value) {
if (value >= 'A' && value <= 'Z') return true;
if (value >= 'a' && value <= 'z') return true;
if (value >= '0' && value <= '9') return true;
return value == '-' || value == '_' || value == '.' || value == '~';
}
String urlEncode(const String &value) {
String encoded;
encoded.reserve(value.length() * 3);
for (size_t index = 0; index < value.length(); index++) {
const char current = value.charAt(index);
if (isUnreservedUrlChar(current)) {
encoded += current;
} else {
encoded += '%';
encoded += HEX_ALPHABET[((uint8_t)current >> 4) & 0x0F];
encoded += HEX_ALPHABET[(uint8_t)current & 0x0F];
}
}
return encoded;
}
int hexNibbleValue(char value) {
if (value >= '0' && value <= '9') return value - '0';
if (value >= 'a' && value <= 'f') return value - 'a' + 10;
if (value >= 'A' && value <= 'F') return value - 'A' + 10;
return -1;
}
String urlDecode(const String &value) {
String decoded;
decoded.reserve(value.length());
for (size_t index = 0; index < value.length(); index++) {
const char current = value.charAt(index);
if (current == '%' && index + 2 < value.length()) {
const int high = hexNibbleValue(value.charAt(index + 1));
const int low = hexNibbleValue(value.charAt(index + 2));
if (high >= 0 && low >= 0) {
decoded += char((high << 4) | low);
index += 2;
} else {
decoded += current;
}
} else if (current == '+') {
decoded += ' ';
} else {
decoded += current;
}
}
return decoded;
}
String jsonEscape(const String &value) {
String escaped;
escaped.reserve(value.length() + 16);
for (size_t index = 0; index < value.length(); index++) {
const char current = value.charAt(index);
if (current == '"' || current == '\\') {
escaped += '\\';
escaped += current;
} else if (current == '\n') {
escaped += F("\\n");
} else if (current == '\r') {
escaped += F("\\r");
} else if (current == '\t') {
escaped += F("\\t");
} else {
escaped += current;
}
}
return escaped;
}
String base64UrlEncode(const String &input) {
String output;
output.reserve(((input.length() + 2) / 3) * 4);
for (size_t index = 0; index < input.length(); index += 3) {
const uint32_t octetA = (uint8_t)input.charAt(index);
const uint32_t octetB = index + 1 < input.length() ? (uint8_t)input.charAt(index + 1) : 0;
const uint32_t octetC = index + 2 < input.length() ? (uint8_t)input.charAt(index + 2) : 0;
const uint32_t triple = (octetA << 16) | (octetB << 8) | octetC;
output += UDC_BASE64URL_ALPHABET[(triple >> 18) & 0x3F];
output += UDC_BASE64URL_ALPHABET[(triple >> 12) & 0x3F];
if (index + 1 < input.length()) output += UDC_BASE64URL_ALPHABET[(triple >> 6) & 0x3F];
if (index + 2 < input.length()) output += UDC_BASE64URL_ALPHABET[triple & 0x3F];
}
return output;
}
int base64UrlDecodeValue(char value) {
if (value >= 'A' && value <= 'Z') return value - 'A';
if (value >= 'a' && value <= 'z') return value - 'a' + 26;
if (value >= '0' && value <= '9') return value - '0' + 52;
if (value == '-') return 62;
if (value == '_') return 63;
return -1;
}
String base64UrlDecode(const String &encoded) {
String output;
output.reserve((encoded.length() * 3) / 4);
size_t index = 0;
while (index < encoded.length()) {
int values[4] = {0, 0, 0, 0};
int count = 0;
while (count < 4 && index < encoded.length()) {
const int decodedValue = base64UrlDecodeValue(encoded.charAt(index++));
if (decodedValue >= 0) values[count++] = decodedValue;
}
if (count >= 2) output += char((values[0] << 2) | (values[1] >> 4));
if (count >= 3) output += char(((values[1] & 0x0F) << 4) | (values[2] >> 2));
if (count >= 4) output += char(((values[2] & 0x03) << 6) | values[3]);
}
return output;
}
String buildRelayIntentJson(bool targetRelayState, const String &challenge, const String &returnUrl) {
const String targetState = targetRelayState ? String(RELAY_STATE_ON_TEXT) : String(RELAY_STATE_OFF_TEXT);
const String targetTitle = targetRelayState ? String(UDC_INTENT_TITLE_ON) : String(UDC_INTENT_TITLE_OFF);
String script;
script.reserve(UDC_INTENT_JSON_RESERVE_BYTES);
script += F("{\"type\":\"script\",");
script += F("\"title\":\"");
script += jsonEscape(targetTitle);
script += F("\",\"description\":\"");
script += jsonEscape(String(UDC_INTENT_DESCRIPTION));
script += F("\",\"exportAs\":\"");
script += UDC_EXPORT_NAME;
script += F("\",\"args\":{\"state\":\"");
script += targetState;
script += F("\",\"challenge\":\"");
script += jsonEscape(challenge);
script += F("\"},\"returnURLPattern\":\"");
script += jsonEscape(returnUrl);
script += F("\",\"return\":{\"mode\":\"last\"},\"encoding\":\"");
script += UDC_ENCODING;
script += F("\",\"run\":{\"address\":{\"type\":\"getCurrentAddress\"},\"sign\":{\"type\":\"signDataWithAddress\",\"address\":\"{get('cache.address')}\",\"dataHex\":\"{strToHex(get('cache.address'))}\"},\"result\":{\"type\":\"macro\",\"run\":\"{sha512(join('',sha512(get('cache.sign.signature')),get('args.challenge'),get('args.state')))}\"}}}");
return script;
}
String encodeGcscriptAsBase64UrlMessage(const String &gcscriptJson) {
String message;
message.reserve(gcscriptJson.length() + sizeof(UDC_BASE64URL_HEADER));
message += UDC_BASE64URL_HEADER;
message += base64UrlEncode(gcscriptJson);
return message;
}
String buildGameChangerConnectionUrl(bool targetRelayState, const String &challenge, const String &returnUrl) {
const String gcscriptJson = buildRelayIntentJson(targetRelayState, challenge, returnUrl);
const String encodedMessage = encodeGcscriptAsBase64UrlMessage(gcscriptJson);
String walletUrl;
walletUrl.reserve(UDC_CONNECTION_URL_RESERVE_BYTES);
walletUrl += UDC_WALLET_API_BASE_URL;
walletUrl += encodedMessage;
walletUrl += F("?networkTag=");
walletUrl += urlEncode(String(UDC_NETWORK_TAG));
return walletUrl;
}
String extractJsonStringField(const String &json, const String &fieldName) {
const String quotedKey = String('"') + fieldName + String('"');
const int keyIndex = json.indexOf(quotedKey);
if (keyIndex < 0) return String();
const int colonIndex = json.indexOf(':', keyIndex + quotedKey.length());
if (colonIndex < 0) return String();
const int firstQuoteIndex = json.indexOf('"', colonIndex + 1);
if (firstQuoteIndex < 0) return String();
String extracted;
extracted.reserve(SHA512_HEX_LENGTH);
for (int index = firstQuoteIndex + 1; index < (int)json.length(); index++) {
const char current = json.charAt(index);
if (current == '"') break;
if (current == '\\' && index + 1 < (int)json.length()) index++;
extracted += json.charAt(index);
}
return extracted;
}
bool looksLikeSha512Hex(const String &value) {
if (value.length() != SHA512_HEX_LENGTH) return false;
for (size_t index = 0; index < value.length(); index++) {
if (hexNibbleValue(value.charAt(index)) < 0) return false;
}
return true;
}
String normalizeMaybeEncodedPayload(const String &rawValue) {
String normalized = urlDecode(rawValue);
normalized.trim();
if (normalized.startsWith(UDC_BASE64URL_HEADER)) {
normalized = base64UrlDecode(normalized.substring(sizeof(UDC_BASE64URL_HEADER) - 1));
normalized.trim();
}
return normalized;
}
String extractExportPayloadFromValue(const String &rawValue) {
const String normalized = normalizeMaybeEncodedPayload(rawValue);
if (looksLikeSha512Hex(normalized)) return normalized;
if (normalized.startsWith(F("{"))) {
const String extracted = extractJsonStringField(normalized, String(UDC_EXPORT_NAME));
if (looksLikeSha512Hex(extracted)) return extracted;
}
return String();
}
String extractExportPayloadFromServerRequest(ESP8266WebServer &server) {
const String exportName = String(UDC_EXPORT_NAME);
if (server.hasArg(exportName)) {
const String directPayload = extractExportPayloadFromValue(server.arg(exportName));
if (directPayload.length() > 0) return directPayload;
}
for (int index = 0; index < server.args(); index++) {
const String valuePayload = extractExportPayloadFromValue(server.arg(index));
if (valuePayload.length() > 0) return valuePayload;
const String namePayload = extractExportPayloadFromValue(server.argName(index));
if (namePayload.length() > 0) return namePayload;
}
return String();
}
#endif