| 1 |
<?php |
| 2 |
/** |
| 3 |
* KeyLockr SSO protocol and cryptographic helpers. |
| 4 |
* |
| 5 |
* @since 4.6.5 |
| 6 |
* @package dologin |
| 7 |
*/ |
| 8 |
|
| 9 |
namespace dologin; |
| 10 |
|
| 11 |
defined( 'WPINC' ) || exit; |
| 12 |
|
| 13 |
trait KLSso_Protocol { |
| 14 |
/** |
| 15 |
* Decode the file key included in the authorization completion. |
| 16 |
*/ |
| 17 |
private function load_file_key( $state, $body ) { |
| 18 |
$packed = $this->bin_value( isset( $body['data_filekey'] ) ? $body['data_filekey'] : '' ); |
| 19 |
if ( ! $packed ) { |
| 20 |
throw new \Exception( __( 'KeyLockr did not return app data filekey.', 'dologin' ) ); |
| 21 |
} |
| 22 |
|
| 23 |
$data = KLSso_MsgPack::unpack( $packed ); |
| 24 |
if ( ! is_array( $data ) || ! isset( $data['encFileKey'], $data['nonceForKey'], $data['apEncPk'] ) ) { |
| 25 |
throw new \Exception( __( 'Invalid KeyLockr app data filekey.', 'dologin' ) ); |
| 26 |
} |
| 27 |
$enc_file_key = $this->bin_value( $data['encFileKey'] ); |
| 28 |
$nonce_for_key = $this->bin_value( $data['nonceForKey'] ); |
| 29 |
$sender_pk = $this->bin_value( $data['apEncPk'] ); |
| 30 |
|
| 31 |
if ( strlen( $enc_file_key ) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES + SODIUM_CRYPTO_BOX_MACBYTES |
| 32 |
|| strlen( $nonce_for_key ) !== SODIUM_CRYPTO_BOX_NONCEBYTES ) { |
| 33 |
throw new \Exception( __( 'Invalid KeyLockr app data filekey.', 'dologin' ) ); |
| 34 |
} |
| 35 |
if ( strlen( $sender_pk ) !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES ) { |
| 36 |
throw new \Exception( __( 'KeyLockr app data filekey is missing sender public key.', 'dologin' ) ); |
| 37 |
} |
| 38 |
|
| 39 |
$enc_sk = empty( $state['enc_sk'] ) ? false : base64_decode( $state['enc_sk'], true ); |
| 40 |
if ( false === $enc_sk || strlen( $enc_sk ) !== SODIUM_CRYPTO_BOX_SECRETKEYBYTES ) { |
| 41 |
throw new \Exception( __( 'Invalid KeyLockr SSO session key.', 'dologin' ) ); |
| 42 |
} |
| 43 |
$keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey( $enc_sk, $sender_pk ); |
| 44 |
$key = sodium_crypto_box_open( $enc_file_key, $nonce_for_key, $keypair ); |
| 45 |
if ( false === $key || strlen( $key ) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES ) { |
| 46 |
throw new \Exception( __( 'Failed to decrypt KeyLockr app data filekey.', 'dologin' ) ); |
| 47 |
} |
| 48 |
|
| 49 |
return $key; |
| 50 |
} |
| 51 |
|
| 52 |
/** |
| 53 |
* Decrypt nonce-prefixed AppData. |
| 54 |
*/ |
| 55 |
private function decrypt_appdata( $data_enc, $file_key ) { |
| 56 |
if ( strlen( $data_enc ) < SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES ) { |
| 57 |
return false; |
| 58 |
} |
| 59 |
$nonce = substr( $data_enc, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ); |
| 60 |
return sodium_crypto_secretbox_open( substr( $data_enc, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ), $nonce, $file_key ); |
| 61 |
} |
| 62 |
|
| 63 |
/** |
| 64 |
* Encrypt AppData with a fresh nonce prefixed to the secretbox ciphertext. |
| 65 |
*/ |
| 66 |
private function encrypt_appdata( $plain, $file_key ) { |
| 67 |
$nonce = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ); |
| 68 |
return $nonce . sodium_crypto_secretbox( $plain, $nonce, $file_key ); |
| 69 |
} |
| 70 |
|
| 71 |
/** |
| 72 |
* Best-effort clear the raw file key after a request. |
| 73 |
*/ |
| 74 |
private function clear_file_key( &$file_key ) { |
| 75 |
if ( $file_key && function_exists( 'sodium_memzero' ) ) { |
| 76 |
sodium_memzero( $file_key ); |
| 77 |
} |
| 78 |
$file_key = ''; |
| 79 |
} |
| 80 |
|
| 81 |
/** |
| 82 |
* Build an AppData read-back verification error. |
| 83 |
*/ |
| 84 |
private function appdata_verification_error() { |
| 85 |
return new \Exception( __( 'KeyLockr app data write could not be verified. Unlock KeyLockr on your phone and try linking again.', 'dologin' ) ); |
| 86 |
} |
| 87 |
|
| 88 |
/** |
| 89 |
* Build an error for exhausted AppData version-conflict retries. |
| 90 |
*/ |
| 91 |
private function appdata_conflict_error() { |
| 92 |
return new \Exception( __( 'KeyLockr app data changed repeatedly while linking. Please scan again.', 'dologin' ) ); |
| 93 |
} |
| 94 |
|
| 95 |
/** |
| 96 |
* Build an error for a service without App Data Storage. |
| 97 |
*/ |
| 98 |
private function appdata_disabled_error() { |
| 99 |
return new \Exception( __( 'KeyLockr app data was not found. This usually means App Data Storage is disabled for this service. Open MyDeveloper, edit this App Tag, enable App Data Storage, and scan again.', 'dologin' ) ); |
| 100 |
} |
| 101 |
|
| 102 |
/** |
| 103 |
* Build a terminal authorization-completion error. |
| 104 |
*/ |
| 105 |
private function app_auth_terminal_error( $message, $authorization_denied = false ) { |
| 106 |
return new \Exception( $message, $authorization_denied ? self::AUTH_DENIED_CODE : self::APP_AUTH_TERMINAL_CODE ); |
| 107 |
} |
| 108 |
|
| 109 |
/** |
| 110 |
* Build an identity mismatch that counts only during a login session. |
| 111 |
*/ |
| 112 |
private function login_identity_error( $message ) { |
| 113 |
return new \Exception( $message, self::LOGIN_IDENTITY_CODE ); |
| 114 |
} |
| 115 |
|
| 116 |
/** |
| 117 |
* Validate an unchanged KeyLockr SERIAL identifier. |
| 118 |
*/ |
| 119 |
private function valid_identity_id( $value ) { |
| 120 |
if ( ! is_string( $value ) || ! preg_match( '/^[1-9][0-9]{0,9}$/D', $value ) ) { |
| 121 |
return false; |
| 122 |
} |
| 123 |
return strlen( $value ) < 10 || strcmp( $value, '2147483647' ) <= 0; |
| 124 |
} |
| 125 |
|
| 126 |
/** |
| 127 |
* Validate an unchanged 36-character KeyLockr handshake identifier. |
| 128 |
*/ |
| 129 |
private function valid_tmp_id( $value ) { |
| 130 |
return is_string( $value ) && 1 === preg_match( '/^[A-Za-z0-9-]{36}$/D', $value ); |
| 131 |
} |
| 132 |
|
| 133 |
/** |
| 134 |
* Whether a terminal completion code explicitly says the user denied authorization. |
| 135 |
*/ |
| 136 |
private function authorization_denied_code( $code ) { |
| 137 |
return in_array( $code, array( 'authorization_denied', 'app_auth_denied', 'safe_auth_denied', 'denied' ), true ); |
| 138 |
} |
| 139 |
|
| 140 |
/** |
| 141 |
* Read the current AppData CAS version. |
| 142 |
*/ |
| 143 |
private function appdata_version( $body ) { |
| 144 |
if ( ! isset( $body['ver'] ) || ! is_string( $body['ver'] ) || ! preg_match( '/^[0-9]+$/', $body['ver'] ) ) { |
| 145 |
throw new \Exception( __( 'KeyLockr returned an invalid app data version.', 'dologin' ) ); |
| 146 |
} |
| 147 |
return $body['ver']; |
| 148 |
} |
| 149 |
|
| 150 |
/** |
| 151 |
* Convert current handshake error codes into actionable messages. |
| 152 |
*/ |
| 153 |
private function handshake_error_message( $body ) { |
| 154 |
$code = isset( $body['code'] ) && is_scalar( $body['code'] ) ? sanitize_text_field( (string) $body['code'] ) : 'unknown_error'; |
| 155 |
if ( 'sso_data_not_approved' === $code ) { |
| 156 |
return __( 'KeyLockr App Data Storage is not approved for this App Tag. Enable it in MyDeveloper and try again.', 'dologin' ); |
| 157 |
} |
| 158 |
if ( in_array( $code, array( 'app_tag_invalid', 'sso_service_invalid' ), true ) ) { |
| 159 |
return __( 'KeyLockr could not resolve this App Tag. Check the effective App Tag in MyDeveloper and try again.', 'dologin' ); |
| 160 |
} |
| 161 |
return sprintf( __( 'KeyLockr handshake failed: %s', 'dologin' ), $code ); |
| 162 |
} |
| 163 |
|
| 164 |
/** |
| 165 |
* Parse and validate capabilities from the authorization result. |
| 166 |
*/ |
| 167 |
private function response_capabilities( $body ) { |
| 168 |
if ( ! isset( $body['capabilities'] ) || ! is_array( $body['capabilities'] ) ) { |
| 169 |
return false; |
| 170 |
} |
| 171 |
$capabilities = array(); |
| 172 |
foreach ( $body['capabilities'] as $capability ) { |
| 173 |
if ( ! is_string( $capability ) || '' === $capability ) { |
| 174 |
return false; |
| 175 |
} |
| 176 |
$capabilities[] = $capability; |
| 177 |
} |
| 178 |
return $capabilities; |
| 179 |
} |
| 180 |
|
| 181 |
/** |
| 182 |
* Read the latest version after an AppData CAS conflict and retry once. |
| 183 |
*/ |
| 184 |
private function retry_appdata_write( &$state ) { |
| 185 |
if ( empty( $state['data_filekey'] ) ) { |
| 186 |
throw $this->appdata_verification_error(); |
| 187 |
} |
| 188 |
$retry_count = isset( $state['appdata_cas_retries'] ) ? (int) $state['appdata_cas_retries'] : 0; |
| 189 |
if ( $retry_count >= self::APPDATA_CAS_RETRY_LIMIT ) { |
| 190 |
throw $this->appdata_conflict_error(); |
| 191 |
} |
| 192 |
$state['appdata_cas_retries'] = $retry_count + 1; |
| 193 |
$state['appdata_retry_pending'] = true; |
| 194 |
return array( |
| 195 |
'status' => 'send', |
| 196 |
'send' => base64_encode( $this->seal_kps( $state, 'app_get_data', array() ) ), |
| 197 |
'message' => __( 'KeyLockr app data changed while linking. Reading the latest version before retrying...', 'dologin' ), |
| 198 |
); |
| 199 |
} |
| 200 |
|
| 201 |
/** |
| 202 |
* Open and validate an incoming KPS frame. |
| 203 |
*/ |
| 204 |
private function open_kps( $state, $frame ) { |
| 205 |
$root = KLSso_MsgPack::unpack( $frame ); |
| 206 |
if ( ! is_array( $root ) || empty( $root['kps'] ) || ! is_array( $root['kps'] ) || empty( $root['seal'] ) ) { |
| 207 |
throw new \Exception( __( 'Invalid KeyLockr KPS frame.', 'dologin' ) ); |
| 208 |
} |
| 209 |
|
| 210 |
$kps = $root['kps']; |
| 211 |
$seal = $this->bin_value( $root['seal'] ); |
| 212 |
$sign_pk = isset( $state['server_sign_pk'] ) ? base64_decode( $state['server_sign_pk'], true ) : false; |
| 213 |
$box = empty( $kps['box'] ) ? '' : $this->bin_value( $kps['box'] ); |
| 214 |
if ( ! $box || empty( $kps['n'] ) || false === $sign_pk || strlen( $sign_pk ) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES || strlen( $seal ) !== SODIUM_CRYPTO_SIGN_BYTES + 32 ) { |
| 215 |
throw new \Exception( __( 'Invalid KeyLockr KPS frame.', 'dologin' ) ); |
| 216 |
} |
| 217 |
$got_hash = sodium_crypto_sign_open( $seal, $sign_pk ); |
| 218 |
if ( false === $got_hash ) { |
| 219 |
throw new \Exception( __( 'Invalid KeyLockr KPS signature.', 'dologin' ) ); |
| 220 |
} |
| 221 |
|
| 222 |
$want_hash = hash( 'sha256', KLSso_MsgPack::pack( $kps ), true ); |
| 223 |
if ( ! hash_equals( $want_hash, $got_hash ) ) { |
| 224 |
throw new \Exception( __( 'Invalid KeyLockr KPS hash.', 'dologin' ) ); |
| 225 |
} |
| 226 |
|
| 227 |
$enc_sk = isset( $state['enc_sk'] ) ? base64_decode( $state['enc_sk'], true ) : false; |
| 228 |
$server_enc_pk = isset( $state['server_enc_pk'] ) ? base64_decode( $state['server_enc_pk'], true ) : false; |
| 229 |
$nonce = $this->bin_value( $kps['n'] ); |
| 230 |
if ( false === $enc_sk || false === $server_enc_pk || strlen( $enc_sk ) !== SODIUM_CRYPTO_BOX_SECRETKEYBYTES || strlen( $server_enc_pk ) !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES || strlen( $nonce ) !== SODIUM_CRYPTO_BOX_NONCEBYTES ) { |
| 231 |
throw new \Exception( __( 'Invalid KeyLockr KPS encryption data.', 'dologin' ) ); |
| 232 |
} |
| 233 |
$keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey( $enc_sk, $server_enc_pk ); |
| 234 |
$plain = sodium_crypto_box_open( $box, $nonce, $keypair ); |
| 235 |
if ( false === $plain ) { |
| 236 |
throw new \Exception( __( 'Failed to decrypt KeyLockr KPS frame.', 'dologin' ) ); |
| 237 |
} |
| 238 |
|
| 239 |
$inner = KLSso_MsgPack::unpack( $plain ); |
| 240 |
if ( ! is_array( $inner ) ) { |
| 241 |
throw new \Exception( __( 'Invalid KeyLockr KPS frame.', 'dologin' ) ); |
| 242 |
} |
| 243 |
$header = isset( $inner['header'] ) && is_array( $inner['header'] ) ? $inner['header'] : array(); |
| 244 |
$body = isset( $inner['body'] ) && is_array( $inner['body'] ) ? $inner['body'] : array(); |
| 245 |
$timestamp = isset( $header['ts'] ) ? filter_var( $header['ts'], FILTER_VALIDATE_INT ) : false; |
| 246 |
if ( empty( $header['from'] ) || false === $timestamp || ! $this->timestamp_valid( (int) $timestamp ) ) { |
| 247 |
throw new \Exception( __( 'Invalid or expired KeyLockr KPS response.', 'dologin' ) ); |
| 248 |
} |
| 249 |
$raw = array(); |
| 250 |
if ( isset( $kps['raw'] ) && ! is_array( $kps['raw'] ) ) { |
| 251 |
throw new \Exception( __( 'Invalid KeyLockr KPS frame.', 'dologin' ) ); |
| 252 |
} |
| 253 |
if ( isset( $kps['raw'] ) ) { |
| 254 |
foreach ( $kps['raw'] as $raw_item ) { |
| 255 |
if ( ! is_string( $raw_item ) && ! ( $raw_item instanceof KLSso_MsgPack_Bin ) ) { |
| 256 |
throw new \Exception( __( 'Invalid KeyLockr KPS frame.', 'dologin' ) ); |
| 257 |
} |
| 258 |
$raw[] = $this->bin_value( $raw_item ); |
| 259 |
} |
| 260 |
$body = $this->join_raw( $body, $raw ); |
| 261 |
} |
| 262 |
|
| 263 |
return array( |
| 264 |
empty( $header['from'] ) ? '' : (string) $header['from'], |
| 265 |
$body, |
| 266 |
); |
| 267 |
} |
| 268 |
|
| 269 |
/** |
| 270 |
* Merge KPS raw binary side-channel fields into the decoded body. |
| 271 |
*/ |
| 272 |
private function join_raw( $value, $raw ) { |
| 273 |
if ( ! is_array( $value ) ) { |
| 274 |
return $value; |
| 275 |
} |
| 276 |
|
| 277 |
foreach ( $value as $key => $item ) { |
| 278 |
if ( is_string( $key ) && substr( $key, -2 ) === '__' && is_int( $item ) && isset( $raw[ $item ] ) ) { |
| 279 |
unset( $value[ $key ] ); |
| 280 |
$value[ substr( $key, 0, -2 ) ] = KLSso_MsgPack::bin( $raw[ $item ] ); |
| 281 |
continue; |
| 282 |
} |
| 283 |
$value[ $key ] = $this->join_raw( $item, $raw ); |
| 284 |
} |
| 285 |
|
| 286 |
return $value; |
| 287 |
} |
| 288 |
|
| 289 |
/** |
| 290 |
* Build an outgoing KPS frame. |
| 291 |
*/ |
| 292 |
private function seal_kps( $state, $action, $body, $raw = array() ) { |
| 293 |
$inner = KLSso_MsgPack::pack( |
| 294 |
array( |
| 295 |
'body' => $body, |
| 296 |
'header' => array( |
| 297 |
'to' => $action, |
| 298 |
'ts' => time(), |
| 299 |
), |
| 300 |
) |
| 301 |
); |
| 302 |
|
| 303 |
$nonce = random_bytes( SODIUM_CRYPTO_BOX_NONCEBYTES ); |
| 304 |
$keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey( base64_decode( $state['enc_sk'] ), base64_decode( $state['server_enc_pk'] ) ); |
| 305 |
$box = sodium_crypto_box( $inner, $nonce, $keypair ); |
| 306 |
$kps = array( |
| 307 |
'app_ver' => 'dologin-' . Core::VER, |
| 308 |
'box' => KLSso_MsgPack::bin( $box ), |
| 309 |
'id' => $state['id'], |
| 310 |
'n' => KLSso_MsgPack::bin( $nonce ), |
| 311 |
); |
| 312 |
if ( $raw ) { |
| 313 |
$kps['raw'] = array(); |
| 314 |
foreach ( $raw as $raw_item ) { |
| 315 |
if ( ! is_string( $raw_item ) && ! ( $raw_item instanceof KLSso_MsgPack_Bin ) ) { |
| 316 |
throw new \Exception( __( 'Invalid KeyLockr KPS raw data.', 'dologin' ) ); |
| 317 |
} |
| 318 |
$kps['raw'][] = KLSso_MsgPack::bin( $this->bin_value( $raw_item ) ); |
| 319 |
} |
| 320 |
} |
| 321 |
$hash = hash( 'sha256', KLSso_MsgPack::pack( $kps ), true ); |
| 322 |
$seal = sodium_crypto_sign( $hash, base64_decode( $state['sign_sk'] ) ); |
| 323 |
|
| 324 |
return KLSso_MsgPack::pack( |
| 325 |
array( |
| 326 |
'kps' => $kps, |
| 327 |
'seal' => KLSso_MsgPack::bin( $seal ), |
| 328 |
) |
| 329 |
); |
| 330 |
} |
| 331 |
|
| 332 |
/** |
| 333 |
* Build a signed WebSocket URL for a KPS identity. |
| 334 |
*/ |
| 335 |
private function ws_url_for( $id, $sign_sk ) { |
| 336 |
$ts = time(); |
| 337 |
$sig = sodium_crypto_sign( $id . '.' . $ts, $sign_sk ); |
| 338 |
return add_query_arg( 'sig', $this->base64url_encode( $sig ), self::ws_url() ); |
| 339 |
} |
| 340 |
|
| 341 |
/** |
| 342 |
* Confirm that the site clock is within the window accepted by KeyLockr. |
| 343 |
*/ |
| 344 |
private function check_server_clock() { |
| 345 |
if ( get_transient( self::CLOCK_CACHE ) ) { |
| 346 |
return true; |
| 347 |
} |
| 348 |
|
| 349 |
$res = wp_safe_remote_get( |
| 350 |
self::api_base() . '/clock', |
| 351 |
array( |
| 352 |
'timeout' => 10, |
| 353 |
'redirection' => 0, |
| 354 |
'limit_response_size' => 1024, |
| 355 |
'sslverify' => true, |
| 356 |
) |
| 357 |
); |
| 358 |
if ( is_wp_error( $res ) ) { |
| 359 |
return $res; |
| 360 |
} |
| 361 |
if ( 200 !== (int) wp_remote_retrieve_response_code( $res ) ) { |
| 362 |
return new \WP_Error( 'dologin_kl_sso_clock_status', __( 'KeyLockr clock request returned an invalid status.', 'dologin' ) ); |
| 363 |
} |
| 364 |
|
| 365 |
$body = json_decode( wp_remote_retrieve_body( $res ), true ); |
| 366 |
$timestamp = is_array( $body ) && isset( $body['ts'] ) ? filter_var( $body['ts'], FILTER_VALIDATE_INT ) : false; |
| 367 |
if ( ! is_array( $body ) || ! isset( $body['_res'] ) || 'ok' !== $body['_res'] || false === $timestamp ) { |
| 368 |
return new \WP_Error( 'dologin_kl_sso_clock_response', __( 'KeyLockr returned invalid clock data.', 'dologin' ) ); |
| 369 |
} |
| 370 |
|
| 371 |
$drift = time() - (int) $timestamp; |
| 372 |
if ( $drift > self::CLOCK_FUTURE_LIMIT || $drift < -self::CLOCK_PAST_LIMIT ) { |
| 373 |
return new \WP_Error( 'dologin_kl_sso_clock_drift', __( 'This server clock is out of sync with KeyLockr. Synchronize the server time and try again.', 'dologin' ) ); |
| 374 |
} |
| 375 |
set_transient( self::CLOCK_CACHE, 1, 10 * MINUTE_IN_SECONDS ); |
| 376 |
return true; |
| 377 |
} |
| 378 |
|
| 379 |
/** |
| 380 |
* Check whether a KeyLockr response timestamp is within the protocol window. |
| 381 |
*/ |
| 382 |
private function timestamp_valid( $timestamp ) { |
| 383 |
$now = time(); |
| 384 |
return $timestamp >= $now - self::CLOCK_PAST_LIMIT && $timestamp <= $now + self::CLOCK_FUTURE_LIMIT; |
| 385 |
} |
| 386 |
|
| 387 |
/** |
| 388 |
* Fetch and cache server public keys. |
| 389 |
*/ |
| 390 |
private function server_keys() { |
| 391 |
$cached = get_transient( self::SERVER_KEYS_CACHE ); |
| 392 |
if ( is_array( $cached ) && ! empty( $cached['enc_pk'] ) && ! empty( $cached['sign_pk'] ) ) { |
| 393 |
$cached_enc = base64_decode( $cached['enc_pk'], true ); |
| 394 |
$cached_sign = base64_decode( $cached['sign_pk'], true ); |
| 395 |
if ( strlen( (string) $cached_enc ) === SODIUM_CRYPTO_BOX_PUBLICKEYBYTES && strlen( (string) $cached_sign ) === SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES ) { |
| 396 |
return array( |
| 397 |
'enc_pk' => $cached_enc, |
| 398 |
'sign_pk' => $cached_sign, |
| 399 |
); |
| 400 |
} |
| 401 |
delete_transient( self::SERVER_KEYS_CACHE ); |
| 402 |
} |
| 403 |
|
| 404 |
$request_args = array( |
| 405 |
'timeout' => 10, |
| 406 |
'redirection' => 1, |
| 407 |
'limit_response_size' => 1024, |
| 408 |
'sslverify' => true, |
| 409 |
); |
| 410 |
$enc = wp_safe_remote_get( self::api_base() . '/key_enc', $request_args ); |
| 411 |
$sign = wp_safe_remote_get( self::api_base() . '/key_sign', $request_args ); |
| 412 |
if ( is_wp_error( $enc ) ) { |
| 413 |
return $enc; |
| 414 |
} |
| 415 |
if ( is_wp_error( $sign ) ) { |
| 416 |
return $sign; |
| 417 |
} |
| 418 |
if ( 200 !== (int) wp_remote_retrieve_response_code( $enc ) || 200 !== (int) wp_remote_retrieve_response_code( $sign ) ) { |
| 419 |
return new \WP_Error( 'dologin_kl_sso_server_key_status', __( 'KeyLockr server key request returned an invalid status.', 'dologin' ) ); |
| 420 |
} |
| 421 |
|
| 422 |
$enc_pk = base64_decode( trim( wp_remote_retrieve_body( $enc ) ), true ); |
| 423 |
$sign_pk = base64_decode( trim( wp_remote_retrieve_body( $sign ) ), true ); |
| 424 |
if ( strlen( (string) $enc_pk ) !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES || strlen( (string) $sign_pk ) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES ) { |
| 425 |
return new \WP_Error( 'dologin_kl_sso_bad_server_keys', __( 'Invalid KeyLockr server public keys.', 'dologin' ) ); |
| 426 |
} |
| 427 |
|
| 428 |
set_transient( |
| 429 |
self::SERVER_KEYS_CACHE, |
| 430 |
array( |
| 431 |
'enc_pk' => base64_encode( $enc_pk ), |
| 432 |
'sign_pk' => base64_encode( $sign_pk ), |
| 433 |
), |
| 434 |
DAY_IN_SECONDS |
| 435 |
); |
| 436 |
|
| 437 |
return array( |
| 438 |
'enc_pk' => $enc_pk, |
| 439 |
'sign_pk' => $sign_pk, |
| 440 |
); |
| 441 |
} |
| 442 |
} |
| 443 |
|