Passed
Push — master ( 634b6b...393309 )
by Morris
16:53 queued 11s
created

Crypto::calculateHMAC()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 2
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2016, ownCloud, Inc.
7
 *
8
 * @author Andreas Fischer <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Lukas Reschke <[email protected]>
11
 * @author lynn-stephenson <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Roeland Jago Douma <[email protected]>
14
 *
15
 * @license AGPL-3.0
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program. If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
31
namespace OC\Security;
32
33
use OCP\IConfig;
34
use OCP\Security\ICrypto;
35
use OCP\Security\ISecureRandom;
36
use phpseclib\Crypt\AES;
37
use phpseclib\Crypt\Hash;
38
39
/**
40
 * Class Crypto provides a high-level encryption layer using AES-CBC. If no key has been provided
41
 * it will use the secret defined in config.php as key. Additionally the message will be HMAC'd.
42
 *
43
 * Usage:
44
 * $encryptWithDefaultPassword = \OC::$server->getCrypto()->encrypt('EncryptedText');
45
 * $encryptWithCustompassword = \OC::$server->getCrypto()->encrypt('EncryptedText', 'password');
46
 *
47
 * @package OC\Security
48
 */
49
class Crypto implements ICrypto {
50
	/** @var AES $cipher */
51
	private $cipher;
52
	/** @var int */
53
	private $ivLength = 16;
54
	/** @var IConfig */
55
	private $config;
56
57
	/**
58
	 * @param IConfig $config
59
	 * @param ISecureRandom $random
60
	 */
61
	public function __construct(IConfig $config) {
62
		$this->cipher = new AES();
63
		$this->config = $config;
64
	}
65
66
	/**
67
	 * @param string $message The message to authenticate
68
	 * @param string $password Password to use (defaults to `secret` in config.php)
69
	 * @return string Calculated HMAC
70
	 */
71
	public function calculateHMAC(string $message, string $password = ''): string {
72
		if ($password === '') {
73
			$password = $this->config->getSystemValue('secret');
74
		}
75
76
		// Append an "a" behind the password and hash it to prevent reusing the same password as for encryption
77
		$password = hash('sha512', $password . 'a');
78
79
		$hash = new Hash('sha512');
80
		$hash->setKey($password);
81
		return $hash->hash($message);
82
	}
83
84
	/**
85
	 * Encrypts a value and adds an HMAC (Encrypt-Then-MAC)
86
	 * @param string $plaintext
87
	 * @param string $password Password to encrypt, if not specified the secret from config.php will be taken
88
	 * @return string Authenticated ciphertext
89
	 */
90
	public function encrypt(string $plaintext, string $password = ''): string {
91
		if ($password === '') {
92
			$password = $this->config->getSystemValue('secret');
93
		}
94
		$keyMaterial = hash_hkdf('sha512', $password);
95
		$this->cipher->setPassword(substr($keyMaterial, 0, 32));
96
97
		$iv = \random_bytes($this->ivLength);
98
		$this->cipher->setIV($iv);
99
100
		$ciphertext = bin2hex($this->cipher->encrypt($plaintext));
101
		$iv = bin2hex($iv);
102
		$hmac = bin2hex($this->calculateHMAC($ciphertext.$iv, substr($keyMaterial, 32)));
103
104
		return $ciphertext.'|'.$iv.'|'.$hmac.'|3';
105
	}
106
107
	/**
108
	 * Decrypts a value and verifies the HMAC (Encrypt-Then-Mac)
109
	 * @param string $authenticatedCiphertext
110
	 * @param string $password Password to encrypt, if not specified the secret from config.php will be taken
111
	 * @return string plaintext
112
	 * @throws \Exception If the HMAC does not match
113
	 * @throws \Exception If the decryption failed
114
	 */
115
	public function decrypt(string $authenticatedCiphertext, string $password = ''): string {
116
		if ($password === '') {
117
			$password = $this->config->getSystemValue('secret');
118
		}
119
		$hmacKey = $encryptionKey = $password;
120
121
		$parts = explode('|', $authenticatedCiphertext);
122
		$partCount = \count($parts);
123
		if ($partCount < 3 || $partCount > 4) {
124
			throw new \Exception('Authenticated ciphertext could not be decoded.');
125
		}
126
127
		$ciphertext = $this->hex2bin($parts[0]);
128
		$iv = $parts[1];
129
		$hmac = $this->hex2bin($parts[2]);
130
131
		if ($partCount === 4) {
132
			$version = $parts[3];
133
			if ($version >= '2') {
134
				$iv = $this->hex2bin($iv);
135
			}
136
137
			if ($version === '3') {
138
				$keyMaterial = hash_hkdf('sha512', $password);
139
				$encryptionKey = substr($keyMaterial, 0, 32);
140
				$hmacKey = substr($keyMaterial, 32);
141
			}
142
		}
143
		$this->cipher->setPassword($encryptionKey);
144
		$this->cipher->setIV($iv);
145
146
		if (!hash_equals($this->calculateHMAC($parts[0] . $parts[1], $hmacKey), $hmac)) {
147
			throw new \Exception('HMAC does not match.');
148
		}
149
150
		$result = $this->cipher->decrypt($ciphertext);
151
		if ($result === false) {
0 ignored issues
show
introduced by
The condition $result === false is always false.
Loading history...
152
			throw new \Exception('Decryption failed');
153
		}
154
155
		return $result;
156
	}
157
158
	private function hex2bin(string $hex): string {
159
		if (!ctype_xdigit($hex)) {
160
			throw new \RuntimeException('String contains non hex chars: ' . $hex);
161
		}
162
		if (strlen($hex) % 2 !== 0) {
163
			throw new \RuntimeException('Hex string is not of even length: ' . $hex);
164
		}
165
		$result = hex2bin($hex);
166
167
		if ($result === false) {
168
			throw new \RuntimeException('Hex to bin conversion failed: ' . $hex);
169
		}
170
171
		return $result;
172
	}
173
}
174