summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/OATHAuth/includes/OATHAuthUtils.php
blob: 2afd3bf8fa554c79995c9d93021b1f1cf4b1a59e (plain)
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
<?php
/**
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 */

/**
 * Utility class for various OATH functions
 *
 * @ingroup Extensions
 */
class OATHAuthUtils {
	/**
	 * Check whether OATH two-factor authentication is enabled for a given user.
	 * This is a stable method that does not change and can be used in other extensions.
	 * @param User $user
	 * @return bool
	 */
	public static function isEnabledFor( User $user ) {
		$oathUser = OATHAuthHooks::getOATHUserRepository()->findByUser( $user );
		return $oathUser && $oathUser->getKey();
	}

	/**
	 * Encrypt an aray of variables to put into the user's session. We use this
	 * when storing the user's password in their session. We can use json as the
	 * serialization format because $plaintextVars is an array of strings.
	 * @param array $plaintextVars array of user input strings
	 * @param int $userId passed to key derivation functions so each user uses
	 * 	distinct encryption and hmac keys
	 * @return string encrypted data packet
	 */
	public static function encryptSessionData( array $plaintextVars, $userId ) {
		$keyMaterial = self::getKeyMaterials();
		$keys = self::getUserKeys( $keyMaterial, $userId );
		return self::seal( json_encode( $plaintextVars ), $keys['encrypt'], $keys['hmac'] );
	}

	/**
	 * Decrypt an encrypted packet, generated with encryptSessionData
	 * @param string $ciphertext Encrypted data packet
	 * @param string|int $userId
	 * @return array of strings
	 */
	public static function decryptSessionData( $ciphertext, $userId ) {
		$keyMaterial = self::getKeyMaterials();
		$keys = self::getUserKeys( $keyMaterial, $userId );
		return json_decode( self::unseal( $ciphertext, $keys['encrypt'], $keys['hmac'] ), true );
	}

	/**
	 * Get the base secret for this wiki, used to derive all of the encryption
	 * keys. When $wgOATHAuthSecret is rotated, users who are part way through the
	 * two-step login will get an exception, and have to re-start the login.
	 * @return string
	 */
	private static function getKeyMaterials() {
		global $wgOATHAuthSecret, $wgSecretKey;
		return $wgOATHAuthSecret ?: $wgSecretKey;
	}

	/**
	 * Generate encryption and hmac keys, unique to this user, based on a single
	 * wiki secret. Use a moderate pbkdf2 work factor in case we ever leak keys.
	 * @param string $secret
	 * @param string|int $userid
	 * @return array including key for encryption and integrity checking
	 */
	private static function getUserKeys( $secret, $userid ) {
		$keymats = hash_pbkdf2( 'sha256', $secret, "oath-$userid", 10001, 64, true );
		return [
			'encrypt' => substr( $keymats, 0, 32 ),
			'hmac' => substr( $keymats, 32, 32 ),
		];
	}

	/**
	 * Actually encrypt the data, using a new random IV, and prepend the hmac
	 * of the encrypted data + IV, using a separate hmac key.
	 * @param string $data
	 * @param string $encKey
	 * @param string $hmacKey
	 * @return string $hmac.$iv.$ciphertext, each component b64 encoded
	 */
	private static function seal( $data, $encKey, $hmacKey ) {
		$iv = MWCryptRand::generate( 16, true );
		$ciphertext = openssl_encrypt(
			$data,
			'aes-256-ctr',
			$encKey,
			OPENSSL_RAW_DATA,
			$iv
		);
		$sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext );
		$hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true );
		return base64_encode( $hmac ) . '.' . $sealed;
	}

	/**
	 * Decrypt data sealed using seal(). First checks the hmac to prevent various
	 * attacks.
	 * @param string $encrypted
	 * @param string $encKey
	 * @param string $hmacKey
	 * @return string plaintext
	 * @throws Exception
	 */
	private static function unseal( $encrypted, $encKey, $hmacKey ) {
		$pieces = explode( '.', $encrypted );
		if ( count( $pieces ) !== 3 ) {
			throw new InvalidArgumentException( 'Invalid sealed-secret format' );
		}

		list( $hmac, $iv, $ciphertext ) = $pieces;
		$integCalc = hash_hmac( 'sha256', $iv . '.' . $ciphertext, $hmacKey, true );
		if ( !hash_equals( $integCalc, base64_decode( $hmac ) ) ) {
			throw new Exception( 'Sealed secret has been tampered with, aborting.' );
		}

		return openssl_decrypt(
			base64_decode( $ciphertext ),
			'aes-256-ctr',
			$encKey,
			OPENSSL_RAW_DATA,
			base64_decode( $iv )
		);
	}

}