Passed
Push — release_2_0 ( 0861a7...27e8ce )
by Stefan
08:12
created

Device_Chromebook::eapBlock()   C

Complexity

Conditions 9
Paths 256

Size

Total Lines 47
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 26
c 2
b 0
f 0
dl 0
loc 47
rs 6.5222
cc 9
nc 256
nop 1
1
<?php
2
3
/*
4
 * *****************************************************************************
5
 * Contributions to this work were made on behalf of the GÉANT project, a 
6
 * project that has received funding from the European Union’s Framework 
7
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
8
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
9
 * 691567 (GN4-1) and No. 731122 (GN4-2).
10
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
11
 * of the copyright in all material which was developed by a member of the GÉANT
12
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
13
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
14
 * UK as a branch of GÉANT Vereniging.
15
 * 
16
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
17
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
18
 *
19
 * License: see the web/copyright.inc.php file in the file structure or
20
 *          <base_url>/copyright.php after deploying the software
21
 */
22
23
/**
24
 * This file contains the TestModule class
25
 *
26
 * This is a very basic example of using the CAT API.  
27
 *
28
 * The module contains two files
29
 * in the Files directory. They will illustrate the use of the {@link DeviceConfig::copyFile()} method.
30
 * One fille will be coppied without the name change, for the second we will provide a new name.
31
 * The API also contains a similar {@link DeviceConfig::translateFile()} method, which is special to Windows installers and not used in this example.
32
 *
33
 * This module will collect all certificate files stored in the database for a given profile and will copy them to the working directory.
34
 *
35
 * If, for the given profile, an information file is available, this will also be copied to the working directory.
36
 *
37
 * The installer will collect all available configuration attributes and save them to a file in the form of the PHP print_r output.
38
 *
39
 * Finally, the installer will create a zip archive containing all above files and this file 
40
 * will be sent to the user as the configurator file.
41
 *
42
 * Go to the {@link Device_TestModule} and {@link DeviceConfig} class definitions to learn more.
43
 *  
44
 * @package ModuleWriting
45
 */
46
47
namespace devices\chromebook;
48
49
use Exception;
50
51
/**
52
 * This is the main implementation class of the module
53
 *
54
 * The name of the class must the the 'Device_' followed by the name of the module file
55
 * (without the '.php' extension), so in this case the file is "TestModule.php" and
56
 * the class is Device_TestModule.
57
 *
58
 * The class MUST define the constructor method and one additional 
59
 * public method: {@link writeInstaller()}.
60
 *
61
 * All other methods and properties should be private. This example sets zipInstaller method to protected, so that it can be seen in the documentation.
62
 *
63
 * It is important to understand how the device module fits into the whole picture, so here is s short descrption.
64
 * An external caller (for instance {@link GUI::generateInstaller()}) creates the module device instance and prepares
65
 * its environment for a given user profile by calling {@link DeviceConfig::setup()} method.
66
 *      this will:
67
 *       - create the temporary directory and save its path as $this->FPATH
68
 *       - process the CA certificates and store results in $this->attributes['internal:CAs'][0]
69
 *            $this->attributes['internal:CAs'][0] is an array of processed CA certificates
70
 *            a processed certifincate is an array 
71
 *               'pem' points to pem feromat certificate
72
 *               'der' points to der format certificate
73
 *               'md5' points to md5 fingerprint
74
 *               'sha1' points to sha1 fingerprint
75
 *               'name' points to the certificate subject
76
 *               'root' can be 1 for self-signed certificate or 0 otherwise
77
 *       - save the info_file (if exists) and put the name in $this->attributes['internal:info_file_name'][0]
78
 * Finally, the module {@link DeviceConfig::writeInstaller ()} is called and the returned path name is used for user download.
79
 *
80
 * @package ModuleWriting
81
 */
82
class Device_Chromebook extends \core\DeviceConfig {
0 ignored issues
show
Coding Style introduced by
This class is not in CamelCase format.

Classes in PHP are usually named in CamelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. The whole name starts with a capital letter as well.

Thus the name database provider becomes DatabaseProvider.

Loading history...
83
84
    /**
85
     * Number of iterations for the PBKDF2 function. 
86
     * 20000 is the minimum as per ChromeOS ONC spec
87
     * 500000 is the maximum as per Chromium source code
88
     * https://cs.chromium.org/chromium/src/chromeos/network/onc/onc_utils.cc?sq=package:chromium&dr=CSs&rcl=1482394814&l=110
89
     */
90
    const PBKDF2_ITERATIONS = 20000;
91
92
    /**
93
     * Constructs a Device object.
94
     *
95
     * @final not to be redefined
96
     */
97
    final public function __construct() {
98
        parent::__construct();
99
        $this->setSupportedEapMethods([\core\common\EAP::EAPTYPE_PEAP_MSCHAP2, \core\common\EAP::EAPTYPE_TTLS_PAP, \core\common\EAP::EAPTYPE_TTLS_MSCHAP2, \core\common\EAP::EAPTYPE_TLS, \core\common\EAP::EAPTYPE_SILVERBULLET]);
100
    }
101
102
    /**
103
     * encrypts the entire configuration. Only used in SB to protect the client
104
     * credential
105
     * 
106
     * @param string $clearJson the cleartext JSON string to encrypt
107
     * @param string $password  the import PIN we told the user
108
     * @return string
109
     */
110
    private function encryptConfig($clearJson, $password) {
111
        $salt = \core\common\Entity::randomString(12);
112
        $encryptionKey = hash_pbkdf2("sha1", $password, $salt, Device_Chromebook::PBKDF2_ITERATIONS, 32, TRUE); // the spec is not clear about the algo. Source code in Chromium makes clear it's SHA1.
113
        $strong = FALSE; // should become TRUE if strong crypto is available like it should.
114
        $initVector = openssl_random_pseudo_bytes(16, $strong);
115
        if ($strong === FALSE) {
116
            $this->loggerInstance->debug(1, "WARNING: OpenSSL reports that a random value was generated with a weak cryptographic algorithm (Device_chromebook::writeInstaller()). You should investigate the reason for this!");
117
        }
118
        $cryptoJson = openssl_encrypt($clearJson, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $initVector);
119
        $hmac = hash_hmac("sha1", $cryptoJson, $encryptionKey, TRUE);
120
121
        $this->loggerInstance->debug(4, "Clear = $clearJson\nSalt = $salt\nPW = " . $password . "\nb(IV) = " . base64_encode($initVector) . "\nb(Cipher) = " . base64_encode($cryptoJson) . "\nb(HMAC) = " . base64_encode($hmac));
122
123
        // now, generate the container that holds all the crypto data
124
        $finalArray = [
125
            "Cipher" => "AES256",
126
            "Ciphertext" => base64_encode($cryptoJson),
127
            "HMAC" => base64_encode($hmac), // again by reading source code! And why?
128
            "HMACMethod" => "SHA1",
129
            "Salt" => base64_encode($salt), // this is B64 encoded, but had to read Chromium source code to find out! Not in the spec!
130
            "Stretch" => "PBKDF2",
131
            "Iterations" => Device_Chromebook::PBKDF2_ITERATIONS,
132
            "IV" => base64_encode($initVector),
133
            "Type" => "EncryptedConfiguration",
134
        ];
135
        return json_encode($finalArray);
136
    }
137
138
    /**
139
     * Creates a WiFi block (SSID based only, no support for Passpoint)
140
     * @param string $ssid       the SSID to configure
141
     * @param array  $eapdetails the EAP sub-block as derived from EapBlock()
142
     * @return array
143
     */
144
    private function wifiBlock($ssid, $eapdetails) {
145
        return [
146
            "GUID" => \core\common\Entity::uuid('', $ssid),
147
            "Name" => "$ssid",
148
            "Remove" => false,
149
            "Type" => "WiFi",
150
            "WiFi" => [
151
                "AutoConnect" => true,
152
                "EAP" => $eapdetails,
153
                "HiddenSSID" => false,
154
                "SSID" => $ssid,
155
                "Security" => "WPA-EAP",
156
            ],
157
            "ProxySettings" => $this->proxySettings(),
158
        ];
159
    }
160
161
    /**
162
     * Creates the ProxySettings block
163
     * 
164
     * @return array
165
     */
166
    protected function proxySettings() {
167
        if (isset($this->attributes['media:force_proxy'])) {
168
            // find the port delimiter. In case of IPv6, there are multiple ':' 
169
            // characters, so we have to find the LAST one
170
            $serverAndPort = explode(':', strrev($this->attributes['media:force_proxy'][0]), 2);
171
            // characters are still reversed, invert on use!
172
            return ["Type" => "Manual",
173
                "Manual" => [
174
                    "SecureHTTPProxy" => [
175
                        "Host" => strrev($serverAndPort[1]),
176
                        "Port" => strrev($serverAndPort[0])
177
                    ]
178
                ]
179
            ];
180
        }
181
        return ["Type" => "WPAD"];
182
    }
183
184
    /**
185
     * Creates a configuration block for wired Ethernet
186
     * 
187
     * @param array $eapdetails the EAP configuration as created with eapBlock()
188
     * @return array
189
     */
190
    private function wiredBlock($eapdetails) {
191
        return [
192
            "GUID" => \core\common\Entity::uuid('', "wired-dot1x-ethernet") . "}",
193
            "Name" => "eduroam configuration (wired network)",
194
            "Remove" => false,
195
            "Type" => "Ethernet",
196
            "Ethernet" => [
197
                "Authentication" => "8021X",
198
                "EAP" => $eapdetails,
199
            ],
200
            "ProxySettings" => ["Type" => "WPAD"],
201
        ];
202
    }
203
204
    /**
205
     * Creates the EAP configuration sub-block
206
     * 
207
     * @param array $caRefs list of strings with CA references
208
     * @return array
209
     */
210
    private function eapBlock($caRefs) {
211
        $selectedEap = $this->selectedEap;
212
        $outerId = $this->determineOuterIdString();
213
        $eapPrettyprint = \core\common\EAP::eapDisplayName($selectedEap);
214
        // ONC has its own enums, and guess what, they don't always match
215
        if ($eapPrettyprint["INNER"] == "MSCHAPV2") {
216
            $eapPrettyprint["INNER"] = "MSCHAPv2";
217
        }
218
        if ($eapPrettyprint["OUTER"] == "TTLS") {
219
            $eapPrettyprint["OUTER"] = "EAP-TTLS";
220
        }
221
        if ($eapPrettyprint["OUTER"] == "TLS") {
222
            $eapPrettyprint["OUTER"] = "EAP-TLS";
223
        }
224
225
        // define EAP properties
226
227
        $eaparray = [];
228
229
        // if silverbullet, we deliver the client cert inline
230
231
        if ($selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
232
            $eaparray['ClientCertRef'] = "[" . $this->clientCert['GUID'] . "]";
233
            $eaparray['ClientCertType'] = "Ref";
234
        }
235
236
        $eaparray["Outer"] = $eapPrettyprint["OUTER"];
237
        if ($eapPrettyprint["INNER"] == "MSCHAPv2") {
238
            $eaparray["Inner"] = $eapPrettyprint["INNER"];
239
        }
240
        $eaparray["SaveCredentials"] = true;
241
        $eaparray["ServerCARefs"] = $caRefs; // maybe takes just one CA?
242
        $eaparray["UseSystemCAs"] = false;
243
        // we can only set one single string, which has to be "contained" in the
244
        // actual incoming server name. This is less secure than wpa_supplicant's
245
        // altSubjectMatch but it is all we have.
246
        if ($this->longestNameSuffix() !== "") {
247
            $eaparray["SubjectMatch"] = $this->longestNameSuffix();
248
        }
249
250
        if ($outerId !== NULL) {
251
            $eaparray["AnonymousIdentity"] = $outerId;
252
        }
253
        if ($selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
254
            $eaparray["Identity"] = $this->clientCert["certObject"]->username;
255
        }
256
        return $eaparray;
257
    }
258
259
    /**
260
     * prepare a ONC file
261
     *
262
     * @return string installer path name
263
     */
0 ignored issues
show
Coding Style Documentation introduced by
Missing @throws tag in function comment
Loading history...
264
    public function writeInstaller() {
265
        $this->loggerInstance->debug(4, "Chromebook Installer start\n");
266
        $caRefs = [];
267
        // we don't do per-user encrypted containers
268
        $jsonArray = ["Type" => "UnencryptedConfiguration"];
269
270
        foreach ($this->attributes['internal:CAs'][0] as $ca) {
271
            $caRefs[] = "{" . $ca['uuid'] . "}";
272
        }
273
        // define CA certificates
274
        foreach ($this->attributes['internal:CAs'][0] as $ca) {
275
            // strip -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----
276
            $this->loggerInstance->debug(3, $ca['pem']);
277
            $caSanitized1 = substr($ca['pem'], 27, strlen($ca['pem']) - 27 - 25 - 1);
278
            if ($caSanitized1 === FALSE) {
279
                throw new Exception("Error cropping PEM data at its BEGIN marker.");
280
            }
281
            $this->loggerInstance->debug(4, $caSanitized1 . "\n");
282
            // remove \n
283
            $caSanitized = str_replace("\n", "", $caSanitized1);
284
            $jsonArray["Certificates"][] = ["GUID" => "{" . $ca['uuid'] . "}", "Remove" => false, "Type" => "Authority", "X509" => $caSanitized];
285
            $this->loggerInstance->debug(3, $caSanitized . "\n");
286
        }
287
        // if we are doing silverbullet, include the unencrypted(!) P12 as a client certificate
288
        if ($this->selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
289
            $jsonArray["Certificates"][] = ["GUID" => "[" . $this->clientCert['GUID'] . "]", "PKCS12" => base64_encode($this->clientCert['certdataclear']), "Remove" => false, "Type" => "Client"];
290
        }
291
        $eaparray = $this->eapBlock($caRefs);
292
        // define Wi-Fi networks
293
        foreach ($this->attributes['internal:SSID'] as $ssid => $cryptolevel) {
294
            $jsonArray["NetworkConfigurations"][] = $this->wifiBlock($ssid, $eaparray);
295
        }
296
        // are we also configuring wired?
297
        if (isset($this->attributes['media:wired'])) {
298
            $jsonArray["NetworkConfigurations"][] = $this->wiredBlock($eaparray);
299
        }
300
301
        $clearJson = json_encode($jsonArray, JSON_PRETTY_PRINT);
302
        $finalJson = $clearJson;
303
        // if we are doing silverbullet we should also encrypt the entire structure(!) with the import password and embed it into a EncryptedConfiguration
304
        if ($this->selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
305
            $finalJson = $this->encryptConfig($clearJson, $this->clientCert['importPassword']);
306
        }
307
308
        file_put_contents('installer_profile', $finalJson);
309
310
        $fileName = $this->installerBasename . '.onc';
311
312
        if (!$this->sign) {
313
            rename("installer_profile", $fileName);
314
            return $fileName;
315
        }
316
317
        // still here? We are signing. That actually can't be - ONC does not
318
        // have the notion of signing
319
        // but if they ever change their mind, we are prepared
320
321
        $outputFromSigning = system($this->sign . " installer_profile '$fileName' > /dev/null");
322
        if ($outputFromSigning === FALSE) {
323
            $this->loggerInstance->debug(2, "Signing the ONC installer $fileName FAILED!\n");
324
        }
325
326
        return $fileName;
327
    }
328
329
    /**
330
     * prepare module desctiption and usage information
331
     * 
332
     * @return string HTML text to be displayed in the information window
333
     */
334
    public function writeDeviceInfo() {
335
        \core\common\Entity::intoThePotatoes();
336
        $out = "<p>";
337
        $out .= _("The installer is a file with the extension '.onc'. Please download it, open Chrome, and navigate to the URL <a href='chrome://net-internals/#chromeos'>chrome://net-internals/#chromeos</a>. Then, use the 'Import ONC file' button. The import is silent; the new network definitions will be added to the preferred networks.");
338
        \core\common\Entity::outOfThePotatoes();
339
        return $out;
340
    }
341
342
}
343