Mercurial
comparison connectors/core.c @ 279:b3b547563ec7
Add Google connector service and agent wiki
Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code.
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 22:22:36 -0700 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 278:8d560f50ed4c | 279:b3b547563ec7 |
|---|---|
| 1 #include "connectors/connector.h" | |
| 2 | |
| 3 #include <openssl/evp.h> | |
| 4 #include <openssl/rand.h> | |
| 5 #include <stdio.h> | |
| 6 #include <string.h> | |
| 7 | |
| 8 static const char BASE64URL[] = | |
| 9 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; | |
| 10 | |
| 11 boolean Connector_Base64Url_Encode( | |
| 12 const uint8 *input, size_t input_length, char *output, size_t output_size) | |
| 13 { | |
| 14 size_t required = (input_length * 4 + 2) / 3; | |
| 15 if (!input || !output || output_size <= required) | |
| 16 return FALSE; | |
| 17 size_t i = 0, j = 0; | |
| 18 while (i + 3 <= input_length) { | |
| 19 uint32 value = ((uint32)input[i] << 16) | | |
| 20 ((uint32)input[i + 1] << 8) | input[i + 2]; | |
| 21 output[j++] = BASE64URL[(value >> 18) & 63]; | |
| 22 output[j++] = BASE64URL[(value >> 12) & 63]; | |
| 23 output[j++] = BASE64URL[(value >> 6) & 63]; | |
| 24 output[j++] = BASE64URL[value & 63]; | |
| 25 i += 3; | |
| 26 } | |
| 27 if (i < input_length) { | |
| 28 uint32 value = (uint32)input[i] << 16; | |
| 29 output[j++] = BASE64URL[(value >> 18) & 63]; | |
| 30 if (i + 1 < input_length) { | |
| 31 value |= (uint32)input[i + 1] << 8; | |
| 32 output[j++] = BASE64URL[(value >> 12) & 63]; | |
| 33 output[j++] = BASE64URL[(value >> 6) & 63]; | |
| 34 } else { | |
| 35 output[j++] = BASE64URL[(value >> 12) & 63]; | |
| 36 } | |
| 37 } | |
| 38 output[j] = '\0'; | |
| 39 return TRUE; | |
| 40 } | |
| 41 | |
| 42 static int32 base64url_value(char c) | |
| 43 { | |
| 44 const char *position = strchr(BASE64URL, c); | |
| 45 return position ? (int32)(position - BASE64URL) : -1; | |
| 46 } | |
| 47 | |
| 48 boolean Connector_Base64Url_Decode( | |
| 49 const char *input, uint8 *output, size_t output_size, size_t *output_length) | |
| 50 { | |
| 51 if (!input || !output || !output_length) | |
| 52 return FALSE; | |
| 53 size_t length = strlen(input); | |
| 54 if ((length % 4) == 1 || output_size < (length * 3) / 4) | |
| 55 return FALSE; | |
| 56 uint32 accumulator = 0; | |
| 57 int32 bits = 0; | |
| 58 size_t written = 0; | |
| 59 for (size_t i = 0; i < length; ++i) { | |
| 60 int32 value = base64url_value(input[i]); | |
| 61 if (value < 0) | |
| 62 return FALSE; | |
| 63 accumulator = (accumulator << 6) | (uint32)value; | |
| 64 bits += 6; | |
| 65 if (bits >= 8) { | |
| 66 bits -= 8; | |
| 67 if (written >= output_size) | |
| 68 return FALSE; | |
| 69 output[written++] = (uint8)((accumulator >> bits) & 255); | |
| 70 } | |
| 71 } | |
| 72 *output_length = written; | |
| 73 return TRUE; | |
| 74 } | |
| 75 | |
| 76 boolean Connector_Encrypt( | |
| 77 const Connector_Master_Key *key, const char *plaintext, | |
| 78 char *encoded, size_t encoded_size) | |
| 79 { | |
| 80 if (!key || !plaintext || !encoded) | |
| 81 return FALSE; | |
| 82 size_t plaintext_length = strlen(plaintext); | |
| 83 if (plaintext_length > 4096) | |
| 84 return FALSE; | |
| 85 uint8 nonce[12], tag[16], ciphertext[4096]; | |
| 86 if (RAND_bytes(nonce, sizeof(nonce)) != 1) | |
| 87 return FALSE; | |
| 88 EVP_CIPHER_CTX *context = EVP_CIPHER_CTX_new(); | |
| 89 int32 length = 0, total = 0; | |
| 90 boolean ok = context && | |
| 91 EVP_EncryptInit_ex(context, EVP_aes_256_gcm(), NULL, NULL, NULL) == 1 && | |
| 92 EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_SET_IVLEN, sizeof(nonce), NULL) == 1 && | |
| 93 EVP_EncryptInit_ex(context, NULL, NULL, key->key, nonce) == 1 && | |
| 94 EVP_EncryptUpdate(context, ciphertext, &length, | |
| 95 (const uint8 *)plaintext, (int32)plaintext_length) == 1; | |
| 96 total = length; | |
| 97 ok = ok && EVP_EncryptFinal_ex(context, ciphertext + total, &length) == 1; | |
| 98 total += length; | |
| 99 ok = ok && EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_GET_TAG, sizeof(tag), tag) == 1; | |
| 100 EVP_CIPHER_CTX_free(context); | |
| 101 if (!ok) | |
| 102 return FALSE; | |
| 103 uint8 envelope[4 + 12 + 16 + 4096]; | |
| 104 envelope[0] = (uint8)(key->version >> 24); | |
| 105 envelope[1] = (uint8)(key->version >> 16); | |
| 106 envelope[2] = (uint8)(key->version >> 8); | |
| 107 envelope[3] = (uint8)key->version; | |
| 108 memcpy(envelope + 4, nonce, sizeof(nonce)); | |
| 109 memcpy(envelope + 16, tag, sizeof(tag)); | |
| 110 memcpy(envelope + 32, ciphertext, (size_t)total); | |
| 111 return Connector_Base64Url_Encode( | |
| 112 envelope, 32 + (size_t)total, encoded, encoded_size); | |
| 113 } | |
| 114 | |
| 115 boolean Connector_Decrypt( | |
| 116 const Connector_Master_Key *key, const char *encoded, | |
| 117 char *plaintext, size_t plaintext_size) | |
| 118 { | |
| 119 uint8 envelope[4 + 12 + 16 + 4096]; | |
| 120 size_t envelope_length = 0; | |
| 121 if (!key || !encoded || !plaintext || | |
| 122 !Connector_Base64Url_Decode( | |
| 123 encoded, envelope, sizeof(envelope), &envelope_length) || | |
| 124 envelope_length < 32) | |
| 125 return FALSE; | |
| 126 uint32 version = ((uint32)envelope[0] << 24) | | |
| 127 ((uint32)envelope[1] << 16) | ((uint32)envelope[2] << 8) | envelope[3]; | |
| 128 size_t ciphertext_length = envelope_length - 32; | |
| 129 if (version != key->version || plaintext_size <= ciphertext_length) | |
| 130 return FALSE; | |
| 131 EVP_CIPHER_CTX *context = EVP_CIPHER_CTX_new(); | |
| 132 int32 length = 0, total = 0; | |
| 133 boolean ok = context && | |
| 134 EVP_DecryptInit_ex(context, EVP_aes_256_gcm(), NULL, NULL, NULL) == 1 && | |
| 135 EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) == 1 && | |
| 136 EVP_DecryptInit_ex(context, NULL, NULL, key->key, envelope + 4) == 1 && | |
| 137 EVP_DecryptUpdate(context, (uint8 *)plaintext, &length, | |
| 138 envelope + 32, (int32)ciphertext_length) == 1; | |
| 139 total = length; | |
| 140 ok = ok && EVP_CIPHER_CTX_ctrl( | |
| 141 context, EVP_CTRL_GCM_SET_TAG, 16, envelope + 16) == 1 && | |
| 142 EVP_DecryptFinal_ex(context, (uint8 *)plaintext + total, &length) == 1; | |
| 143 total += length; | |
| 144 EVP_CIPHER_CTX_free(context); | |
| 145 if (!ok) { | |
| 146 memset(plaintext, 0, plaintext_size); | |
| 147 return FALSE; | |
| 148 } | |
| 149 plaintext[total] = '\0'; | |
| 150 return TRUE; | |
| 151 } | |
| 152 | |
| 153 boolean Connector_Form_Encode( | |
| 154 const char *input, char *output, size_t output_size) | |
| 155 { | |
| 156 static const char hex[] = "0123456789ABCDEF"; | |
| 157 if (!input || !output) | |
| 158 return FALSE; | |
| 159 size_t j = 0; | |
| 160 for (size_t i = 0; input[i]; ++i) { | |
| 161 uint8 c = (uint8)input[i]; | |
| 162 boolean safe = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || | |
| 163 (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~'; | |
| 164 size_t needed = safe ? 1 : 3; | |
| 165 if (j + needed >= output_size) | |
| 166 return FALSE; | |
| 167 if (safe) | |
| 168 output[j++] = (char)c; | |
| 169 else { | |
| 170 output[j++] = '%'; | |
| 171 output[j++] = hex[c >> 4]; | |
| 172 output[j++] = hex[c & 15]; | |
| 173 } | |
| 174 } | |
| 175 output[j] = '\0'; | |
| 176 return TRUE; | |
| 177 } | |
| 178 | |
| 179 const char *Connector_Operation_Name(Connector_Operation operation) | |
| 180 { | |
| 181 static const char *names[] = { | |
| 182 "drive.list", "drive.get", "drive.download", "drive.changes", | |
| 183 "drive.create", "drive.upload", "drive.update", "gmail.list", | |
| 184 "gmail.get", "gmail.attachment", "gmail.history", "gmail.draft.create", | |
| 185 "gmail.send" | |
| 186 }; | |
| 187 return operation >= CONNECTOR_OP_DRIVE_LIST && | |
| 188 operation <= CONNECTOR_OP_GMAIL_SEND ? names[operation] : "unknown"; | |
| 189 } | |
| 190 | |
| 191 boolean Connector_Operation_Is_Mutation(Connector_Operation operation) | |
| 192 { | |
| 193 return operation == CONNECTOR_OP_DRIVE_CREATE || | |
| 194 operation == CONNECTOR_OP_DRIVE_UPLOAD || | |
| 195 operation == CONNECTOR_OP_DRIVE_UPDATE || | |
| 196 operation == CONNECTOR_OP_GMAIL_DRAFT_CREATE || | |
| 197 operation == CONNECTOR_OP_GMAIL_SEND; | |
| 198 } | |
| 199 | |
| 200 boolean Connector_Operation_Is_Allowed(Connector_Operation operation) | |
| 201 { | |
| 202 return operation >= CONNECTOR_OP_DRIVE_LIST && | |
| 203 operation <= CONNECTOR_OP_GMAIL_SEND; | |
| 204 } | |
| 205 | |
| 206 boolean Connector_Request_Digest( | |
| 207 const char *user_id, const char *account_id, Connector_Operation operation, | |
| 208 const Connector_Provider_Request *request, char output[65]) | |
| 209 { | |
| 210 if (!user_id || !account_id || !request || !output || | |
| 211 request->body_length > CONNECTOR_MAX_JSON_BYTES) | |
| 212 return FALSE; | |
| 213 EVP_MD_CTX *context = EVP_MD_CTX_new(); | |
| 214 uint8 digest[32]; | |
| 215 uint32 digest_length = 0; | |
| 216 const char separator = '\0'; | |
| 217 #define HASH_FIELD(value, length) do { \ | |
| 218 EVP_DigestUpdate(context, (value) ? (value) : "", (value) ? (length) : 0); \ | |
| 219 EVP_DigestUpdate(context, &separator, 1); \ | |
| 220 } while (0) | |
| 221 boolean ok = context && | |
| 222 EVP_DigestInit_ex(context, EVP_sha256(), NULL) == 1; | |
| 223 if (!ok) { | |
| 224 EVP_MD_CTX_free(context); | |
| 225 return FALSE; | |
| 226 } | |
| 227 const char *name = Connector_Operation_Name(operation); | |
| 228 HASH_FIELD(user_id, strlen(user_id)); | |
| 229 HASH_FIELD(account_id, strlen(account_id)); | |
| 230 HASH_FIELD(name, strlen(name)); | |
| 231 HASH_FIELD(request->method, request->method ? strlen(request->method) : 0); | |
| 232 HASH_FIELD(request->path, request->path ? strlen(request->path) : 0); | |
| 233 HASH_FIELD(request->query, request->query ? strlen(request->query) : 0); | |
| 234 HASH_FIELD(request->content_type, | |
| 235 request->content_type ? strlen(request->content_type) : 0); | |
| 236 HASH_FIELD(request->body, request->body_length); | |
| 237 uint8 overwrite = request->overwrite ? 1 : 0; | |
| 238 EVP_DigestUpdate(context, &overwrite, 1); | |
| 239 ok = EVP_DigestFinal_ex(context, digest, &digest_length) == 1; | |
| 240 EVP_MD_CTX_free(context); | |
| 241 #undef HASH_FIELD | |
| 242 if (!ok || digest_length != 32) | |
| 243 return FALSE; | |
| 244 for (size_t i = 0; i < sizeof(digest); ++i) | |
| 245 snprintf(output + i * 2, 3, "%02x", digest[i]); | |
| 246 output[64] = '\0'; | |
| 247 return TRUE; | |
| 248 } | |
| 249 | |
| 250 boolean Connector_OAuth_PKCE_Start(Connector_OAuth_Start *start) | |
| 251 { | |
| 252 uint8 state[32], verifier[48], digest[32]; | |
| 253 uint32 digest_length = 0; | |
| 254 if (!start || RAND_bytes(state, sizeof(state)) != 1 || | |
| 255 RAND_bytes(verifier, sizeof(verifier)) != 1 || | |
| 256 !Connector_Base64Url_Encode( | |
| 257 state, sizeof(state), start->state, sizeof(start->state)) || | |
| 258 !Connector_Base64Url_Encode( | |
| 259 verifier, sizeof(verifier), start->code_verifier, | |
| 260 sizeof(start->code_verifier))) | |
| 261 return FALSE; | |
| 262 if (EVP_Digest( | |
| 263 start->code_verifier, strlen(start->code_verifier), digest, | |
| 264 &digest_length, EVP_sha256(), NULL) != 1 || digest_length != 32) | |
| 265 return FALSE; | |
| 266 return Connector_Base64Url_Encode( | |
| 267 digest, sizeof(digest), start->code_challenge, | |
| 268 sizeof(start->code_challenge)); | |
| 269 } |