diff options
Diffstat (limited to 'src/sp_cookie_encryption.c')
| -rw-r--r-- | src/sp_cookie_encryption.c | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/src/sp_cookie_encryption.c b/src/sp_cookie_encryption.c new file mode 100644 index 0000000..5248486 --- /dev/null +++ b/src/sp_cookie_encryption.c | |||
| @@ -0,0 +1,216 @@ | |||
| 1 | #include "php_snuffleupagus.h" | ||
| 2 | |||
| 3 | #include "ext/standard/url.h" | ||
| 4 | |||
| 5 | ZEND_DECLARE_MODULE_GLOBALS(snuffleupagus) | ||
| 6 | |||
| 7 | static unsigned int nonce_d = 0; | ||
| 8 | |||
| 9 | static inline void generate_key(unsigned char *key) { | ||
| 10 | PHP_SHA256_CTX ctx; | ||
| 11 | const char *user_agent = sp_getenv("HTTP_USER_AGENT"); | ||
| 12 | const char *remote_addr = sp_getenv("REMOTE_ADDR"); | ||
| 13 | const char *encryption_key = | ||
| 14 | SNUFFLEUPAGUS_G(config).config_snuffleupagus->encryption_key; | ||
| 15 | |||
| 16 | /* 32 is the size of a SHA256. */ | ||
| 17 | assert(32 == crypto_secretbox_KEYBYTES); | ||
| 18 | |||
| 19 | PHP_SHA256Init(&ctx); | ||
| 20 | |||
| 21 | if (user_agent) { | ||
| 22 | PHP_SHA256Update(&ctx, (unsigned char *)user_agent, strlen(user_agent)); | ||
| 23 | } | ||
| 24 | |||
| 25 | if (remote_addr) { | ||
| 26 | char out[128]; | ||
| 27 | apply_mask_on_ip(out, remote_addr); | ||
| 28 | PHP_SHA256Update(&ctx, (unsigned char*)out, sizeof(out)); | ||
| 29 | } | ||
| 30 | |||
| 31 | if (encryption_key) { | ||
| 32 | PHP_SHA256Update(&ctx, (const unsigned char *)encryption_key, | ||
| 33 | strlen(encryption_key)); | ||
| 34 | } | ||
| 35 | |||
| 36 | PHP_SHA256Final((unsigned char *)key, &ctx); | ||
| 37 | } | ||
| 38 | |||
| 39 | int decrypt_cookie(zval *pDest, int num_args, va_list args, | ||
| 40 | zend_hash_key *hash_key) { | ||
| 41 | unsigned char key[crypto_secretbox_KEYBYTES] = {0}; | ||
| 42 | size_t value_len; | ||
| 43 | zend_string *debase64; | ||
| 44 | unsigned char *decrypted; | ||
| 45 | int ret = 0; | ||
| 46 | |||
| 47 | /* If the cookie isn't in the conf, it shouldn't be encrypted. */ | ||
| 48 | if (0 == | ||
| 49 | zend_hash_exists(SNUFFLEUPAGUS_G(config).config_cookie_encryption->names, | ||
| 50 | hash_key->key)) { | ||
| 51 | return ZEND_HASH_APPLY_KEEP; | ||
| 52 | } | ||
| 53 | |||
| 54 | generate_key(key); | ||
| 55 | |||
| 56 | value_len = php_url_decode(Z_STRVAL_P(pDest), Z_STRLEN_P(pDest)); | ||
| 57 | |||
| 58 | if (value_len == 0) { | ||
| 59 | return ZEND_HASH_APPLY_KEEP; | ||
| 60 | } | ||
| 61 | |||
| 62 | debase64 = php_base64_decode((unsigned char *)(Z_STRVAL_P(pDest)), value_len); | ||
| 63 | |||
| 64 | if (value_len < | ||
| 65 | crypto_secretbox_NONCEBYTES + crypto_secretbox_ZEROBYTES) { | ||
| 66 | sp_log_msg("cookie_encryption", LOG_DROP, | ||
| 67 | "Buffer underflow tentative detected in cookie encryption handling."); | ||
| 68 | return ZEND_HASH_APPLY_REMOVE; | ||
| 69 | } | ||
| 70 | |||
| 71 | decrypted = pecalloc(value_len, 1, 0); | ||
| 72 | |||
| 73 | ret = crypto_secretbox_open( | ||
| 74 | decrypted, | ||
| 75 | (unsigned char *)(ZSTR_VAL(debase64) + crypto_secretbox_NONCEBYTES), | ||
| 76 | ZSTR_LEN(debase64) - crypto_secretbox_NONCEBYTES, | ||
| 77 | (unsigned char *)ZSTR_VAL(debase64), key); | ||
| 78 | |||
| 79 | if (ret == -1) { | ||
| 80 | sp_log_msg("cookie_encryption", LOG_DROP, | ||
| 81 | "Something went wrong with the decryption of %s.", | ||
| 82 | ZSTR_VAL(hash_key->key)); | ||
| 83 | return ZEND_HASH_APPLY_REMOVE; | ||
| 84 | } | ||
| 85 | |||
| 86 | ZVAL_STRINGL(pDest, (char *)(decrypted + crypto_secretbox_ZEROBYTES), | ||
| 87 | ZSTR_LEN(debase64) - crypto_secretbox_NONCEBYTES - 1 - | ||
| 88 | crypto_secretbox_ZEROBYTES); | ||
| 89 | |||
| 90 | return ZEND_HASH_APPLY_KEEP; | ||
| 91 | } | ||
| 92 | |||
| 93 | /** | ||
| 94 | This function will return the `data` of length `data_len` encrypted in the | ||
| 95 | form | ||
| 96 | base64(nonce | encrypted_data) (with `|` being the concatenation | ||
| 97 | operation). | ||
| 98 | |||
| 99 | The `nonce` is time-based. | ||
| 100 | */ | ||
| 101 | static zend_string *encrypt_data(char *data, unsigned long long data_len) { | ||
| 102 | const size_t encrypted_msg_len = crypto_secretbox_ZEROBYTES + data_len + 1; | ||
| 103 | const size_t emsg_and_nonce_len = encrypted_msg_len + crypto_secretbox_NONCEBYTES; | ||
| 104 | |||
| 105 | unsigned char key[crypto_secretbox_KEYBYTES] = {0}; | ||
| 106 | unsigned char nonce[crypto_secretbox_NONCEBYTES] = {0}; | ||
| 107 | unsigned char *data_to_encrypt = pecalloc(encrypted_msg_len, 1, 0); | ||
| 108 | unsigned char *encrypted_data = pecalloc(emsg_and_nonce_len, 1, 1); | ||
| 109 | |||
| 110 | generate_key(key); | ||
| 111 | |||
| 112 | /* tweetnacl's API requires the message to be padded with | ||
| 113 | crypto_secretbox_ZEROBYTES zeroes. */ | ||
| 114 | memcpy(data_to_encrypt + crypto_secretbox_ZEROBYTES, data, data_len); | ||
| 115 | |||
| 116 | assert(sizeof(size_t) <= crypto_secretbox_NONCEBYTES); | ||
| 117 | |||
| 118 | nonce_d++; | ||
| 119 | sscanf((char*)nonce, "%ud", &nonce_d); | ||
| 120 | |||
| 121 | memcpy(encrypted_data, nonce, crypto_secretbox_NONCEBYTES); | ||
| 122 | crypto_secretbox(encrypted_data + crypto_secretbox_NONCEBYTES, | ||
| 123 | data_to_encrypt, encrypted_msg_len, nonce, key); | ||
| 124 | |||
| 125 | zend_string *z = php_base64_encode(encrypted_data, emsg_and_nonce_len); | ||
| 126 | sp_log_debug("cookie_encryption", "Cookie value:%s:", z->val); | ||
| 127 | return z; | ||
| 128 | } | ||
| 129 | |||
| 130 | PHP_FUNCTION(sp_setcookie) { | ||
| 131 | zval params[7] = { 0 }; | ||
| 132 | zend_string *name = NULL, *value = NULL, *path = NULL, *domain = NULL; | ||
| 133 | zend_long expires = 0; | ||
| 134 | zend_bool secure = 0, httponly = 0; | ||
| 135 | zval ret_val; | ||
| 136 | zval func_name; | ||
| 137 | |||
| 138 | ZEND_PARSE_PARAMETERS_START(1, 7) | ||
| 139 | Z_PARAM_STR(name) | ||
| 140 | Z_PARAM_OPTIONAL | ||
| 141 | Z_PARAM_STR(value) | ||
| 142 | Z_PARAM_LONG(expires) | ||
| 143 | Z_PARAM_STR(path) | ||
| 144 | Z_PARAM_STR(domain) | ||
| 145 | Z_PARAM_BOOL(secure) | ||
| 146 | Z_PARAM_BOOL(httponly) | ||
| 147 | ZEND_PARSE_PARAMETERS_END(); | ||
| 148 | |||
| 149 | /* If the request was issued over HTTPS, the cookie should be "secure" */ | ||
| 150 | if (SNUFFLEUPAGUS_G(config).config_auto_cookie_secure) { | ||
| 151 | const zval server_vars = PG(http_globals)[TRACK_VARS_SERVER]; | ||
| 152 | if (Z_TYPE(server_vars) == IS_ARRAY) { | ||
| 153 | const zval *is_https = | ||
| 154 | zend_hash_str_find(Z_ARRVAL(server_vars), "HTTPS", strlen("HTTPS")); | ||
| 155 | if (NULL != is_https) { | ||
| 156 | secure = 1; | ||
| 157 | } | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | /* If the cookie's value is encrypted, it won't be usable by | ||
| 162 | * javascript anyway. | ||
| 163 | */ | ||
| 164 | if (zend_hash_exists(SNUFFLEUPAGUS_G(config).config_cookie_encryption->names, | ||
| 165 | name) > 0) { | ||
| 166 | httponly = 1; | ||
| 167 | } | ||
| 168 | |||
| 169 | /* Shall we encrypt the cookie's value? */ | ||
| 170 | if (zend_hash_exists(SNUFFLEUPAGUS_G(config).config_cookie_encryption->names, | ||
| 171 | name) > 0 && value) { | ||
| 172 | zend_string *encrypted_data = encrypt_data(value->val, value->len); | ||
| 173 | ZVAL_STR_COPY(¶ms[1], encrypted_data); | ||
| 174 | zend_string_release(encrypted_data); | ||
| 175 | } else if (value) { | ||
| 176 | ZVAL_STR_COPY(¶ms[1], value); | ||
| 177 | } | ||
| 178 | |||
| 179 | ZVAL_STRING(&func_name, "setcookie"); | ||
| 180 | ZVAL_STR_COPY(¶ms[0], name); | ||
| 181 | ZVAL_LONG(¶ms[2], expires); | ||
| 182 | if (path) { | ||
| 183 | ZVAL_STR_COPY(¶ms[3], path); | ||
| 184 | } | ||
| 185 | if (domain) { | ||
| 186 | ZVAL_STR_COPY(¶ms[4], domain); | ||
| 187 | } | ||
| 188 | if (secure) { | ||
| 189 | ZVAL_LONG(¶ms[5], secure); | ||
| 190 | } | ||
| 191 | if (httponly) { | ||
| 192 | ZVAL_LONG(¶ms[6], httponly); | ||
| 193 | } | ||
| 194 | |||
| 195 | /* This is the _fun_ part: because PHP is utterly idiotic and nonsensical, | ||
| 196 | the `call_user_function` macro will __discard__ (yes) its first argument | ||
| 197 | (the hashtable), effectively calling functions from `CG(function_table)`. | ||
| 198 | This is why were replacing our hook with the original function, calling | ||
| 199 | the function, and then re-hooking it. */ | ||
| 200 | void (*handler)(INTERNAL_FUNCTION_PARAMETERS); | ||
| 201 | handler = zend_hash_str_find_ptr(SNUFFLEUPAGUS_G(sp_internal_functions_hook), "setcookie", | ||
| 202 | strlen("setcookie")); | ||
| 203 | zend_internal_function *func = zend_hash_str_find_ptr( | ||
| 204 | CG(function_table), "setcookie", strlen("setcookie")); | ||
| 205 | func->handler = handler; | ||
| 206 | |||
| 207 | call_user_function(CG(function_table), NULL, &func_name, &ret_val, 7, params); | ||
| 208 | |||
| 209 | func->handler = PHP_FN(sp_setcookie); | ||
| 210 | } | ||
| 211 | |||
| 212 | int hook_cookies() { | ||
| 213 | HOOK_FUNCTION("setcookie", sp_internal_functions_hook, PHP_FN(sp_setcookie), false); | ||
| 214 | |||
| 215 | return SUCCESS; | ||
| 216 | } | ||
