Passed
Push — master ( 7a0ac3...8d02ee )
by Morris
14:29 queued 11s
created

CertificateManager::getCertificateBundle()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 2
rs 10
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bjoern Schiessle <[email protected]>
6
 * @author Björn Schießle <[email protected]>
7
 * @author Christoph Wurst <[email protected]>
8
 * @author Joas Schilling <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Robin Appelman <[email protected]>
12
 * @author Roeland Jago Douma <[email protected]>
13
 *
14
 * @license AGPL-3.0
15
 *
16
 * This code is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License, version 3,
18
 * as published by the Free Software Foundation.
19
 *
20
 * This program is distributed in the hope that it will be useful,
21
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
 * GNU Affero General Public License for more details.
24
 *
25
 * You should have received a copy of the GNU Affero General Public License, version 3,
26
 * along with this program. If not, see <http://www.gnu.org/licenses/>
27
 *
28
 */
29
30
namespace OC\Security;
31
32
use OC\Files\Filesystem;
33
use OCP\ICertificateManager;
34
use OCP\IConfig;
35
use OCP\ILogger;
36
use OCP\Security\ISecureRandom;
37
38
/**
39
 * Manage trusted certificates for users
40
 */
41
class CertificateManager implements ICertificateManager {
42
	/**
43
	 * @var \OC\Files\View
44
	 */
45
	protected $view;
46
47
	/**
48
	 * @var IConfig
49
	 */
50
	protected $config;
51
52
	/**
53
	 * @var ILogger
54
	 */
55
	protected $logger;
56
57
	/** @var ISecureRandom */
58
	protected $random;
59
60
	/**
61
	 * @param \OC\Files\View $view relative to data/
62
	 * @param IConfig $config
63
	 * @param ILogger $logger
64
	 * @param ISecureRandom $random
65
	 */
66
	public function __construct(\OC\Files\View $view,
67
								IConfig $config,
68
								ILogger $logger,
69
								ISecureRandom $random) {
70
		$this->view = $view;
71
		$this->config = $config;
72
		$this->logger = $logger;
73
		$this->random = $random;
74
	}
75
76
	/**
77
	 * Returns all certificates trusted by the user
78
	 *
79
	 * @return \OCP\ICertificate[]
80
	 */
81
	public function listCertificates() {
82
		if (!$this->config->getSystemValue('installed', false)) {
83
			return [];
84
		}
85
86
		$path = $this->getPathToCertificates() . 'uploads/';
87
		if (!$this->view->is_dir($path)) {
88
			return [];
89
		}
90
		$result = [];
91
		$handle = $this->view->opendir($path);
92
		if (!is_resource($handle)) {
93
			return [];
94
		}
95
		while (false !== ($file = readdir($handle))) {
96
			if ($file != '.' && $file != '..') {
97
				try {
98
					$result[] = new Certificate($this->view->file_get_contents($path . $file), $file);
99
				} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
100
				}
101
			}
102
		}
103
		closedir($handle);
104
		return $result;
105
	}
106
107
	private function hasCertificates(): bool {
108
		if (!$this->config->getSystemValue('installed', false)) {
109
			return false;
110
		}
111
112
		$path = $this->getPathToCertificates() . 'uploads/';
113
		if (!$this->view->is_dir($path)) {
114
			return false;
115
		}
116
		$result = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
117
		$handle = $this->view->opendir($path);
118
		if (!is_resource($handle)) {
119
			return false;
120
		}
121
		while (false !== ($file = readdir($handle))) {
122
			if ($file !== '.' && $file !== '..') {
123
				return true;
124
			}
125
		}
126
		closedir($handle);
127
		return false;
128
	}
129
130
	/**
131
	 * create the certificate bundle of all trusted certificated
132
	 */
133
	public function createCertificateBundle() {
134
		$path = $this->getPathToCertificates();
135
		$certs = $this->listCertificates();
136
137
		if (!$this->view->file_exists($path)) {
138
			$this->view->mkdir($path);
139
		}
140
141
		$defaultCertificates = file_get_contents(\OC::$SERVERROOT . '/resources/config/ca-bundle.crt');
142
		if (strlen($defaultCertificates) < 1024) { // sanity check to verify that we have some content for our bundle
143
			// log as exception so we have a stacktrace
144
			$this->logger->logException(new \Exception('Shipped ca-bundle is empty, refusing to create certificate bundle'));
145
			return;
146
		}
147
148
		$certPath = $path . 'rootcerts.crt';
149
		$tmpPath = $certPath . '.tmp' . $this->random->generate(10, ISecureRandom::CHAR_DIGITS);
150
		$fhCerts = $this->view->fopen($tmpPath, 'w');
151
152
		// Write user certificates
153
		foreach ($certs as $cert) {
154
			$file = $path . '/uploads/' . $cert->getName();
155
			$data = $this->view->file_get_contents($file);
156
			if (strpos($data, 'BEGIN CERTIFICATE')) {
157
				fwrite($fhCerts, $data);
158
				fwrite($fhCerts, "\r\n");
159
			}
160
		}
161
162
		// Append the default certificates
163
		fwrite($fhCerts, $defaultCertificates);
164
165
		// Append the system certificate bundle
166
		$systemBundle = $this->getCertificateBundle();
167
		if ($systemBundle !== $certPath && $this->view->file_exists($systemBundle)) {
168
			$systemCertificates = $this->view->file_get_contents($systemBundle);
169
			fwrite($fhCerts, $systemCertificates);
170
		}
171
172
		fclose($fhCerts);
173
174
		$this->view->rename($tmpPath, $certPath);
175
	}
176
177
	/**
178
	 * Save the certificate and re-generate the certificate bundle
179
	 *
180
	 * @param string $certificate the certificate data
181
	 * @param string $name the filename for the certificate
182
	 * @return \OCP\ICertificate
183
	 * @throws \Exception If the certificate could not get added
184
	 */
185
	public function addCertificate($certificate, $name) {
186
		if (!Filesystem::isValidPath($name) or Filesystem::isFileBlacklisted($name)) {
187
			throw new \Exception('Filename is not valid');
188
		}
189
190
		$dir = $this->getPathToCertificates() . 'uploads/';
191
		if (!$this->view->file_exists($dir)) {
192
			$this->view->mkdir($dir);
193
		}
194
195
		try {
196
			$file = $dir . $name;
197
			$certificateObject = new Certificate($certificate, $name);
198
			$this->view->file_put_contents($file, $certificate);
199
			$this->createCertificateBundle();
200
			return $certificateObject;
201
		} catch (\Exception $e) {
202
			throw $e;
203
		}
204
	}
205
206
	/**
207
	 * Remove the certificate and re-generate the certificate bundle
208
	 *
209
	 * @param string $name
210
	 * @return bool
211
	 */
212
	public function removeCertificate($name) {
213
		if (!Filesystem::isValidPath($name)) {
214
			return false;
215
		}
216
		$path = $this->getPathToCertificates() . 'uploads/';
217
		if ($this->view->file_exists($path . $name)) {
218
			$this->view->unlink($path . $name);
219
			$this->createCertificateBundle();
220
		}
221
		return true;
222
	}
223
224
	/**
225
	 * Get the path to the certificate bundle
226
	 *
227
	 * @return string
228
	 */
229
	public function getCertificateBundle() {
230
		return $this->getPathToCertificates() . 'rootcerts.crt';
231
	}
232
233
	/**
234
	 * Get the full local path to the certificate bundle
235
	 *
236
	 * @return string
237
	 */
238
	public function getAbsoluteBundlePath() {
239
		if (!$this->hasCertificates()) {
240
			return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
241
		}
242
243
		if ($this->needsRebundling()) {
244
			$this->createCertificateBundle();
245
		}
246
247
		return $this->view->getLocalFile($this->getCertificateBundle());
248
	}
249
250
	/**
251
	 * @return string
252
	 */
253
	private function getPathToCertificates() {
254
		return '/files_external/';
255
	}
256
257
	/**
258
	 * Check if we need to re-bundle the certificates because one of the sources has updated
259
	 *
260
	 * @return bool
261
	 */
262
	private function needsRebundling() {
263
		$targetBundle = $this->getCertificateBundle();
264
		if (!$this->view->file_exists($targetBundle)) {
265
			return true;
266
		}
267
268
		$sourceMTime = $this->getFilemtimeOfCaBundle();
269
		return $sourceMTime > $this->view->filemtime($targetBundle);
270
	}
271
272
	/**
273
	 * get mtime of ca-bundle shipped by Nextcloud
274
	 *
275
	 * @return int
276
	 */
277
	protected function getFilemtimeOfCaBundle() {
278
		return filemtime(\OC::$SERVERROOT . '/resources/config/ca-bundle.crt');
279
	}
280
}
281