PluginProbe
DoLogin Security / trunk
DoLogin Security vtrunk
5.0.10 4.8.3 trunk 1.0 1.1 1.1.1 1.2 1.2.1 1.2.2 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 1.6 All 64 releases
dologin / src / secret.cls.php

secret.cls.php in DoLogin Security trunk, at src/secret.cls.php

154 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Salt-bound secret storage helpers.
4 *
5 * @since 4.7.4
6 * @package dologin
7 */
8
9 namespace dologin;
10
11 defined( 'WPINC' ) || exit;
12
13 class Secret {
14 const TOKEN_PREFIX = 'dologin:hmac:v1:';
15 const SODIUM_PREFIX = 'dologin:sealed:v1:s:';
16 const OPENSSL_PREFIX = 'dologin:sealed:v1:o:';
17
18 /**
19 * Create a non-reversible token verifier bound to the WordPress authentication salt.
20 */
21 public static function token_hash( $purpose, $token ) {
22 if ( ! is_string( $purpose ) || '' === $purpose || ! is_string( $token ) || '' === $token ) {
23 return false;
24 }
25 $key = hash_hmac( 'sha256', 'dologin-token-key-v1|' . $purpose, wp_salt( 'auth' ), true );
26 return self::TOKEN_PREFIX . hash_hmac( 'sha256', $token, $key );
27 }
28
29 /**
30 * Verify a raw bearer token without storing the token itself.
31 */
32 public static function verify_token( $purpose, $token, $stored ) {
33 if ( ! self::is_token_hash( $stored ) ) {
34 return false;
35 }
36 $expected = self::token_hash( $purpose, $token );
37 return is_string( $expected ) && hash_equals( $stored, $expected );
38 }
39
40 /**
41 * Whether a value is a versioned token verifier.
42 */
43 public static function is_token_hash( $stored ) {
44 return is_string( $stored )
45 && 1 === preg_match( '/^' . preg_quote( self::TOKEN_PREFIX, '/' ) . '[a-f0-9]{64}$/D', $stored );
46 }
47
48 /**
49 * Encrypt and authenticate a recoverable secret with a purpose-bound site key.
50 */
51 public static function seal( $purpose, $plain ) {
52 if ( ! is_string( $purpose ) || '' === $purpose || ! is_string( $plain ) || '' === $plain ) {
53 return false;
54 }
55
56 if ( function_exists( 'sodium_crypto_secretbox' )
57 && defined( 'SODIUM_CRYPTO_SECRETBOX_NONCEBYTES' )
58 && defined( 'SODIUM_CRYPTO_SECRETBOX_KEYBYTES' ) ) {
59 try {
60 $nonce = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
61 $box = sodium_crypto_secretbox( $plain, $nonce, self::storage_key( $purpose, SODIUM_CRYPTO_SECRETBOX_KEYBYTES ) );
62 } catch ( \Exception $ex ) {
63 return false;
64 }
65 return self::SODIUM_PREFIX . base64_encode( $nonce . $box );
66 }
67
68 if ( function_exists( 'openssl_encrypt' ) && defined( 'OPENSSL_RAW_DATA' ) ) {
69 $cipher_name = 'aes-256-cbc';
70 $iv_length = openssl_cipher_iv_length( $cipher_name );
71 if ( ! is_int( $iv_length ) || $iv_length <= 0 ) {
72 return false;
73 }
74 try {
75 $iv = random_bytes( $iv_length );
76 } catch ( \Exception $ex ) {
77 return false;
78 }
79 $keys = self::storage_key( $purpose, 64 );
80 $cipher = openssl_encrypt( $plain, $cipher_name, substr( $keys, 0, 32 ), OPENSSL_RAW_DATA, $iv );
81 if ( false === $cipher ) {
82 return false;
83 }
84 $tag = hash_hmac( 'sha256', self::OPENSSL_PREFIX . $purpose . '|' . $iv . $cipher, substr( $keys, 32, 32 ), true );
85 return self::OPENSSL_PREFIX . base64_encode( $iv . $tag . $cipher );
86 }
87
88 return false;
89 }
90
91 /**
92 * Authenticate and decrypt a versioned secret.
93 */
94 public static function open( $purpose, $stored ) {
95 if ( ! is_string( $purpose ) || '' === $purpose || ! is_string( $stored ) ) {
96 return false;
97 }
98
99 if ( 0 === strpos( $stored, self::SODIUM_PREFIX ) ) {
100 if ( ! function_exists( 'sodium_crypto_secretbox_open' )
101 || ! defined( 'SODIUM_CRYPTO_SECRETBOX_NONCEBYTES' )
102 || ! defined( 'SODIUM_CRYPTO_SECRETBOX_MACBYTES' )
103 || ! defined( 'SODIUM_CRYPTO_SECRETBOX_KEYBYTES' ) ) {
104 return false;
105 }
106 $sealed = base64_decode( substr( $stored, strlen( self::SODIUM_PREFIX ) ), true );
107 if ( false === $sealed || strlen( $sealed ) < SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES ) {
108 return false;
109 }
110 $nonce = substr( $sealed, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
111 return sodium_crypto_secretbox_open( substr( $sealed, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ), $nonce, self::storage_key( $purpose, SODIUM_CRYPTO_SECRETBOX_KEYBYTES ) );
112 }
113
114 if ( 0 === strpos( $stored, self::OPENSSL_PREFIX ) ) {
115 if ( ! function_exists( 'openssl_decrypt' ) || ! defined( 'OPENSSL_RAW_DATA' ) ) {
116 return false;
117 }
118 $cipher_name = 'aes-256-cbc';
119 $iv_length = openssl_cipher_iv_length( $cipher_name );
120 $sealed = base64_decode( substr( $stored, strlen( self::OPENSSL_PREFIX ) ), true );
121 if ( ! is_int( $iv_length ) || $iv_length <= 0 || false === $sealed || strlen( $sealed ) <= $iv_length + 32 ) {
122 return false;
123 }
124 $iv = substr( $sealed, 0, $iv_length );
125 $tag = substr( $sealed, $iv_length, 32 );
126 $cipher = substr( $sealed, $iv_length + 32 );
127 $keys = self::storage_key( $purpose, 64 );
128 $expected_tag = hash_hmac( 'sha256', self::OPENSSL_PREFIX . $purpose . '|' . $iv . $cipher, substr( $keys, 32, 32 ), true );
129 if ( ! hash_equals( $expected_tag, $tag ) ) {
130 return false;
131 }
132 return openssl_decrypt( $cipher, $cipher_name, substr( $keys, 0, 32 ), OPENSSL_RAW_DATA, $iv );
133 }
134
135 return false;
136 }
137
138 /**
139 * Whether a value uses one of the supported encrypted formats.
140 */
141 public static function is_sealed( $stored ) {
142 return is_string( $stored )
143 && ( 0 === strpos( $stored, self::SODIUM_PREFIX ) || 0 === strpos( $stored, self::OPENSSL_PREFIX ) );
144 }
145
146 /**
147 * Derive purpose-separated key material from the WordPress authentication salt.
148 */
149 private static function storage_key( $purpose, $length ) {
150 $material = hash_hmac( 'sha512', 'dologin-storage-key-v1|' . $purpose, wp_salt( 'auth' ), true );
151 return substr( $material, 0, $length );
152 }
153 }
154