Completed
Pull Request — master (#42)
by rugk
03:18
created

CryptToolTests   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 225
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 17
c 1
b 0
f 0
lcom 1
cbo 5
dl 0
loc 225
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A testCreateKeyPair() 0 9 1
A testRandomNonce() 0 6 1
B testDecrypt() 0 24 2
A testDerivePublicKey() 0 8 1
B testEncrypt() 0 27 1
A testEncryptImage() 0 19 1
A testHexBin() 0 23 1
A testRemoveVar() 0 14 2
A doTest() 0 15 3
B testCompare() 0 52 4
1
<?php
2
/**
3
 * @author Threema GmbH
4
 * @copyright Copyright (c) 2015-2016 Threema GmbH
5
 */
6
7
8
9
namespace Threema\MsgApi\Tests;
10
11
use Threema\Console\Common;
12
use Threema\MsgApi\Messages\TextMessage;
13
use Threema\MsgApi\Tools\CryptTool;
14
15
class CryptToolTests extends \PHPUnit_Framework_TestCase {
16
17
	/**
18
	 * test generating key pair
19
	 */
20
	public function testCreateKeyPair() {
21
		$this->doTest(function(CryptTool $cryptTool, $prefix) {
22
			$this->assertNotNull($cryptTool, $prefix.' could not instance crypto tool');
23
			$keyPair = $cryptTool->generateKeyPair();
24
			$this->assertNotNull($keyPair, $prefix.': invalid key pair');
25
			$this->assertNotNull($keyPair->privateKey, $prefix.': private key is null');
26
			$this->assertNotNull($keyPair->publicKey, $prefix.': public key is null');
27
		});
28
	}
29
30
	/**
31
	 * test generating random nonce
32
	 */
33
	public function testRandomNonce() {
34
		$this->doTest(function(CryptTool $cryptTool, $prefix) {
35
			$randomNonce = $cryptTool->randomNonce();
36
			$this->assertEquals(24, strlen($randomNonce), $prefix.': random nonce size not 24');
37
		});
38
	}
39
40
	public function testDecrypt() {
41
		/** @noinspection PhpUnusedParameterInspection */
42
		$this->doTest(function(CryptTool $cryptTool, $prefix) {
0 ignored issues
show
Unused Code introduced by
The parameter $prefix is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
43
			$nonce = '0a1ec5b67b4d61a1ef91f55e8ce0471fee96ea5d8596dfd0';
44
			$box = '45181c7aed95a1c100b1b559116c61b43ce15d04014a805288b7d14bf3a993393264fe554794ce7d6007233e8ef5a0f1ccdd704f34e7c7b77c72c239182caf1d061d6fff6ffbbfe8d3b8f3475c2fe352e563aa60290c666b2e627761e32155e62f048b52ef2f39c13ac229f393c67811749467396ecd09f42d32a4eb419117d0451056ac18fac957c52b0cca67568e2d97e5a3fd829a77f914a1ad403c5909fd510a313033422ea5db71eaf43d483238612a54cb1ecfe55259b1de5579e67c6505df7d674d34a737edf721ea69d15b567bc2195ec67e172f3cb8d6842ca88c29138cc33e9351dbc1e4973a82e1cf428c1c763bb8f3eb57770f914a';
45
46
			$privateKey = Common::getPrivateKey(Constants::otherPrivateKey);
47
			$this->assertNotNull($privateKey);
48
49
			$publicKey = Common::getPublicKey(Constants::myPublicKey);
50
			$this->assertNotNull($publicKey);
51
52
			$message = $cryptTool->decryptMessage($cryptTool->hex2bin($box),
53
				$cryptTool->hex2bin($privateKey),
54
				$cryptTool->hex2bin($publicKey),
55
				$cryptTool->hex2bin($nonce));
56
57
			$this->assertNotNull($message);
58
			$this->assertTrue($message instanceof TextMessage);
59
			if($message instanceof TextMessage) {
60
				$this->assertEquals($message->getText(), 'Dies ist eine Testnachricht. äöü');
61
			}
62
		});
63
	}
64
65
	public function testEncrypt() {
66
		/** @noinspection PhpUnusedParameterInspection */
67
		$this->doTest(function(CryptTool $cryptTool, $prefix) {
0 ignored issues
show
Unused Code introduced by
The parameter $prefix is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
68
			$text = 'Dies ist eine Testnachricht. äöü';
69
			$nonce = '0a1ec5b67b4d61a1ef91f55e8ce0471fee96ea5d8596dfd0';
70
71
			$privateKey = Common::getPrivateKey(Constants::myPrivateKey);
72
			$this->assertNotNull($privateKey);
73
74
			$publicKey = Common::getPublicKey(Constants::otherPublicKey);
75
			$this->assertNotNull($publicKey);
76
77
			$message = $cryptTool->encryptMessageText($text,
78
				$cryptTool->hex2bin($privateKey),
79
				$cryptTool->hex2bin($publicKey),
80
				$cryptTool->hex2bin($nonce));
81
82
			$this->assertNotNull($message);
83
84
			$box = $cryptTool->decryptMessage($message,
85
				$cryptTool->hex2bin(Common::getPrivateKey(Constants::otherPrivateKey)),
86
				$cryptTool->hex2bin(Common::getPublicKey(Constants::myPublicKey)),
87
				$cryptTool->hex2bin($nonce));
88
89
			$this->assertNotNull($box);
90
		});
91
	}
92
93
94
	public function testDerivePublicKey() {
95
		$this->doTest(function(CryptTool $cryptTool, $prefix){
96
			$publicKey = $cryptTool->derivePublicKey($cryptTool->hex2bin(Common::getPrivateKey(Constants::myPrivateKey)));
97
			$myPublicKey = $cryptTool->hex2bin(Common::getPublicKey(Constants::myPublicKey));
98
99
			$this->assertEquals($publicKey, $myPublicKey, $prefix.' derive public key failed');
100
		});
101
	}
102
103
	public function testEncryptImage() {
104
		$threemaIconContent = file_get_contents(dirname(__FILE__).'/threema.jpg');
105
106
		/** @noinspection PhpUnusedParameterInspection */
107
		$this->doTest(function(CryptTool $cryptTool, $prefix) use($threemaIconContent) {
0 ignored issues
show
Unused Code introduced by
The parameter $prefix is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
108
			$privateKey = $cryptTool->hex2bin(Common::getPrivateKey(Constants::myPrivateKey));
109
			$publicKey = $cryptTool->hex2bin(Common::getPublicKey(Constants::myPublicKey));
110
111
			$otherPrivateKey = $cryptTool->hex2bin(Common::getPrivateKey(Constants::otherPrivateKey));
112
			$otherPublicKey = $cryptTool->hex2bin(Common::getPublicKey(Constants::otherPublicKey));
113
114
			$result = $cryptTool->encryptImage($threemaIconContent, $privateKey, $otherPublicKey);
115
116
			$decryptedImage = $cryptTool->decryptImage($result->getData(), $publicKey, $otherPrivateKey, $result->getNonce());
117
118
			$this->assertEquals($decryptedImage, $threemaIconContent, 'decryption of image failed');
119
120
		});
121
	}
122
123
	/**
124
	 * test hex2bin and bin2hex
125
	 */
126
	public function testHexBin() {
127
		$this->doTest(function(CryptTool $cryptTool, $prefix) {
128
			$testStr = Constants::myPrivateKeyExtract;
129
130
			// convert hex to bin
131
			$testStrBin = $cryptTool->hex2bin($testStr);
132
			$this->assertNotNull($testStrBin);
133
			$testStrBinPhp = hex2bin($testStr);
134
135
			// compare usual PHP conversion with crypt tool version
136
			$this->assertEquals($testStrBin, $testStrBinPhp, $prefix.': hex2bin returns different result than PHP-only implementation');
137
138
			// convert back to hex
139
			$testStrHex = $cryptTool->bin2hex($testStrBin);
140
			$this->assertNotNull($testStrHex);
141
			$testStrHexPhp = bin2hex($testStrBin);
142
143
			// compare usual PHP conversion with crypt tool version
144
			$this->assertEquals($testStrHex, $testStrHexPhp, $prefix.': bin2hex returns different result than PHP-only implementation');
145
			// compare with initial value
146
			$this->assertEquals($testStr, $testStrHex, $prefix.': binary string is different than initial string after conversions');
147
		});
148
	}
149
150
	/**
151
	 * test compare functions to make sure they are resistant to timing attacks
152
	 */
153
	public function testCompare() {
154
		$this->doTest(function(CryptTool $cryptTool, $prefix) {
155
				// make strings large enough
156
				$string1 = str_repeat(Constants::myPrivateKey, 100000);
157
				$string2 = str_repeat(Constants::otherPrivateKey, 100000);
158
				echo PHP_EOL;
159
160
				$humanDescr = [
161
					'length' => 'different length',
162
					'diff' => 'same length, different content',
163
					'same' => 'same length, same content'
164
				];
165
166
				// test different strings when comparing and get time needed
167
				$result = [];
168
				foreach(array(
169
					'length' => [$string1, $string1 . 'a'],
170
					'diff' => [$string1, $string2],
171
					'same' => [$string1, $string1]
172
				) as $testName => $strings) {
173
					$timeStart = microtime(true);
174
					$comparisonResult = $cryptTool->stringCompare($strings[0], $strings[1]);
175
					$timeEnd = microtime(true);
176
					$timeElapsed = $timeEnd - $timeStart;
177
178
					// echo $prefix.': '.$humanDescr[$testName].': '.$timeElapsed.'; result: '.$comparisonResult.PHP_EOL;
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
179
					$result[$testName] = [$timeElapsed, $comparisonResult];
180
181
					// check result
182
					if ($testName == 'length' || $testName == 'diff') {
183
						$this->assertEquals($comparisonResult, false, $prefix.': comparison of "'.$humanDescr[$testName].'" is wrong: expected: false, got '.$comparisonResult);
184
					} else {
185
						$this->assertEquals($comparisonResult, true, $prefix.': comparison of "'.$humanDescr[$testName].'" is wrong: expected: true, got '.$comparisonResult);
186
					}
187
				}
188
189
				// check timings
190
				echo 'Timing test results with '.$prefix.':'.PHP_EOL;
191
				$timingRatio = 2 - ($result['diff'][0] / $result['same'][0]);
192
				$absoluteDifference = abs($result['diff'][0] - $result['same'][0]);
193
				echo 'timing ratio: '.$timingRatio.PHP_EOL;
194
				echo 'absolute difference: '.$absoluteDifference.PHP_EOL;
195
196
				// only allow 20% relative difference of two values
197
				$allowedDifference = 0.20;
198
				$this->assertLessThan(1+$allowedDifference, $timingRatio, $prefix.': difference of comparison ration of "'.$humanDescr['diff'].'" compared to "'.$humanDescr['same'].'" is too high. Ration: '.$timingRatio);
199
				$this->assertGreaterThan(1-$allowedDifference, $timingRatio, $prefix.': difference of comparison ration of "'.$humanDescr['diff'].'" compared to "'.$humanDescr['same'].'" is too high. Ration: '.$timingRatio);
200
201
				// make sure the absolute difference is smaller than 0.06 microseconds
202
				$this->assertLessThan(0.6, $absoluteDifference, $prefix.': difference of comparison ration of "'.$humanDescr['diff'].'" compared to "'.$humanDescr['same'].'" is too high. Value is: '.$absoluteDifference.' micro seconds');
203
			});
204
	}
205
206
	/**
207
	 * test variable deletion
208
	 */
209
	public function testRemoveVar() {
210
		$this->doTest(function(CryptTool $cryptTool, $prefix) {
211
			foreach(array(
212
						'hex' => Constants::myPrivateKeyExtract,
213
						'bin' => $cryptTool->hex2bin(Constants::myPrivateKeyExtract)
214
					) as $key => $testVar) {
215
				// let it remove
216
				$cryptTool->removeVar($testVar);
217
218
				$this->assertEmpty($testVar, $prefix.': variable is not empty (test: '.$key.')');
219
				$this->assertNull($testVar, $prefix.': variable is not null (test: '.$key.')');
220
			}
221
		});
222
	}
223
224
	private function doTest(\Closure $c) {
225
		foreach(array(
226
					'Salt' => CryptTool::createInstance(CryptTool::TYPE_SALT),
227
					'Sodium' => CryptTool::createInstance(CryptTool::TYPE_SODIUM)
228
				) as $key => $instance) {
229
230
			if($instance === null) {
231
				echo $key.": could not instance crypt tool\n";
232
				break;
233
			}
234
			/** @noinspection PhpUndefinedMethodInspection */
235
			$this->assertTrue($instance->isSupported(), $key.' not supported');
236
			$c->__invoke($instance, $key);
237
		}
238
	}
239
}
240