Test Setup Failed
Push — master ( 5548b8...63dc2d )
by Stefan
16:44
created

DeviceChromebook::proxySettings()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 16
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
1
<?php
2
/*
3
 * *****************************************************************************
4
 * Contributions to this work were made on behalf of the GÉANT project, a 
5
 * project that has received funding from the European Union’s Framework 
6
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
7
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
8
 * 691567 (GN4-1) and No. 731122 (GN4-2).
9
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
10
 * of the copyright in all material which was developed by a member of the GÉANT
11
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
12
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
13
 * UK as a branch of GÉANT Vereniging.
14
 * 
15
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
16
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
17
 *
18
 * License: see the web/copyright.inc.php file in the file structure or
19
 *          <base_url>/copyright.php after deploying the software
20
 */
21
22
/**
23
 * This file contains the TestModule class
24
 *
25
 * This is a very basic example of using the CAT API.  
26
 *
27
 * The module contains two files
28
 * in the Files directory. They will illustrate the use of the {@link DeviceConfig::copyFile()} method.
29
 * One fille will be coppied without the name change, for the second we will provide a new name.
30
 * The API also contains a similar {@link DeviceConfig::translateFile()} method, which is special to Windows installers and not used in this example.
31
 *
32
 * This module will collect all certificate files stored in the database for a given profile and will copy them to the working directory.
33
 *
34
 * If, for the given profile, an information file is available, this will also be copied to the working directory.
35
 *
36
 * The installer will collect all available configuration attributes and save them to a file in the form of the PHP print_r output.
37
 *
38
 * Finally, the installer will create a zip archive containing all above files and this file 
39
 * will be sent to the user as the configurator file.
40
 *
41
 * Go to the {@link Device_TestModule} and {@link DeviceConfig} class definitions to learn more.
42
 *  
43
 * @package ModuleWriting
44
 */
45
46
namespace devices\chromebook;
47
48
use Exception;
49
50
/**
51
 * This is the main implementation class of the module
52
 *
53
 * The name of the class must the the 'Device_' followed by the name of the module file
54
 * (without the '.php' extension), so in this case the file is "TestModule.php" and
55
 * the class is Device_TestModule.
56
 *
57
 * The class MUST define the constructor method and one additional 
58
 * public method: {@link writeInstaller()}.
59
 *
60
 * All other methods and properties should be private. This example sets zipInstaller method to protected, so that it can be seen in the documentation.
61
 *
62
 * It is important to understand how the device module fits into the whole picture, so here is s short descrption.
63
 * An external caller (for instance {@link GUI::generateInstaller()}) creates the module device instance and prepares
64
 * its environment for a given user profile by calling {@link DeviceConfig::setup()} method.
65
 *      this will:
66
 *       - create the temporary directory and save its path as $this->FPATH
67
 *       - process the CA certificates and store results in $this->attributes['internal:CAs'][0]
68
 *            $this->attributes['internal:CAs'][0] is an array of processed CA certificates
69
 *            a processed certifincate is an array 
70
 *               'pem' points to pem feromat certificate
71
 *               'der' points to der format certificate
72
 *               'md5' points to md5 fingerprint
73
 *               'sha1' points to sha1 fingerprint
74
 *               'name' points to the certificate subject
75
 *               'root' can be 1 for self-signed certificate or 0 otherwise
76
 *       - save the info_file (if exists) and put the name in $this->attributes['internal:info_file_name'][0]
77
 * Finally, the module {@link DeviceConfig::writeInstaller ()} is called and the returned path name is used for user download.
78
 *
79
 * @package ModuleWriting
80
 */
81
class DeviceChromebook extends \core\DeviceConfig {
82
83
    /**
84
     * Number of iterations for the PBKDF2 function. 
85
     * 20000 is the minimum as per ChromeOS ONC spec
86
     * 500000 is the maximum as per Chromium source code
87
     * https://cs.chromium.org/chromium/src/chromeos/network/onc/onc_utils.cc?sq=package:chromium&dr=CSs&rcl=1482394814&l=110
88
     */
89
    const PBKDF2_ITERATIONS = 20000;
90
91
    /**
92
     * Constructs a Device object.
93
     *
94
     * @final not to be redefined
95
     */
96
    final public function __construct() {
97
        parent::__construct();
98
        $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]);
99
    }
100
101
    /**
102
     * encrypts the entire configuration. Only used in SB to protect the client
103
     * credential
104
     * 
105
     * @param string $clearJson the cleartext JSON string to encrypt
106
     * @param string $password  the import PIN we told the user
107
     * @return string
108
     */
109
    private function encryptConfig($clearJson, $password) {
110
        $salt = \core\common\Entity::randomString(12);
111
        $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.
0 ignored issues
show
Bug introduced by
The type devices\chromebook\Device_Chromebook was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
112
        $strong = FALSE; // should become TRUE if strong crypto is available like it should.
113
        $initVector = openssl_random_pseudo_bytes(16, $strong);
114
        if ($strong === FALSE) {
115
            $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!");
116
        }
117
        $cryptoJson = openssl_encrypt($clearJson, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $initVector);
118
        $hmac = hash_hmac("sha1", $cryptoJson, $encryptionKey, TRUE);
119
120
        $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));
121
122
        // now, generate the container that holds all the crypto data
123
        $finalArray = [
124
            "Cipher" => "AES256",
125
            "Ciphertext" => base64_encode($cryptoJson),
126
            "HMAC" => base64_encode($hmac), // again by reading source code! And why?
127
            "HMACMethod" => "SHA1",
128
            "Salt" => base64_encode($salt), // this is B64 encoded, but had to read Chromium source code to find out! Not in the spec!
129
            "Stretch" => "PBKDF2",
130
            "Iterations" => Device_Chromebook::PBKDF2_ITERATIONS,
131
            "IV" => base64_encode($initVector),
132
            "Type" => "EncryptedConfiguration",
133
        ];
134
        return json_encode($finalArray);
135
    }
136
137
    /**
138
     * Creates a WiFi block (SSID based only, no support for Passpoint)
139
     * @param string $ssid       the SSID to configure
140
     * @param array  $eapdetails the EAP sub-block as derived from EapBlock()
141
     * @return array
142
     */
143
    private function wifiBlock($ssid, $eapdetails) {
144
        return [
145
            "GUID" => \core\common\Entity::uuid('', $ssid),
146
            "Name" => "$ssid",
147
            "Remove" => false,
148
            "Type" => "WiFi",
149
            "WiFi" => [
150
                "AutoConnect" => true,
151
                "EAP" => $eapdetails,
152
                "HiddenSSID" => false,
153
                "SSID" => $ssid,
154
                "Security" => "WPA-EAP",
155
            ],
156
            "ProxySettings" => $this->proxySettings(),
157
        ];
158
    }
159
160
    /**
161
     * Creates the ProxySettings block
162
     * 
163
     * @return array
164
     */
165
    protected function proxySettings() {
166
        if (isset($this->attributes['media:force_proxy'])) {
167
            // find the port delimiter. In case of IPv6, there are multiple ':' 
168
            // characters, so we have to find the LAST one
169
            $serverAndPort = explode(':', strrev($this->attributes['media:force_proxy'][0]), 2);
170
            // characters are still reversed, invert on use!
171
            return ["Type" => "Manual",
172
                "Manual" => [
173
                    "SecureHTTPProxy" => [
174
                        "Host" => strrev($serverAndPort[1]),
175
                        "Port" => strrev($serverAndPort[0])
176
                    ]
177
                ]
178
            ];
179
        }
180
        return ["Type" => "WPAD"];
181
    }
182
183
    /**
184
     * Creates a configuration block for wired Ethernet
185
     * 
186
     * @param array $eapdetails the EAP configuration as created with eapBlock()
187
     * @return array
188
     */
189
    private function wiredBlock($eapdetails) {
190
        return [
191
            "GUID" => \core\common\Entity::uuid('', "wired-dot1x-ethernet") . "}",
192
            "Name" => "eduroam configuration (wired network)",
193
            "Remove" => false,
194
            "Type" => "Ethernet",
195
            "Ethernet" => [
196
                "Authentication" => "8021X",
197
                "EAP" => $eapdetails,
198
            ],
199
            "ProxySettings" => ["Type" => "WPAD"],
200
        ];
201
    }
202
203
    /**
204
     * Creates the EAP configuration sub-block
205
     * 
206
     * @param array $caRefs list of strings with CA references
207
     * @return array
208
     */
209
    private function eapBlock($caRefs) {
210
        $selectedEap = $this->selectedEap;
211
        $outerId = $this->determineOuterIdString();
212
        $eapPrettyprint = \core\common\EAP::eapDisplayName($selectedEap);
213
        // ONC has its own enums, and guess what, they don't always match
214
        if ($eapPrettyprint["INNER"] == "MSCHAPV2") {
215
            $eapPrettyprint["INNER"] = "MSCHAPv2";
216
        }
217
        if ($eapPrettyprint["OUTER"] == "TTLS") {
218
            $eapPrettyprint["OUTER"] = "EAP-TTLS";
219
        }
220
        if ($eapPrettyprint["OUTER"] == "TLS") {
221
            $eapPrettyprint["OUTER"] = "EAP-TLS";
222
        }
223
224
        // define EAP properties
225
226
        $eaparray = [];
227
228
        // if silverbullet, we deliver the client cert inline
229
230
        if ($selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
231
            $eaparray['ClientCertRef'] = "[" . $this->clientCert['GUID'] . "]";
232
            $eaparray['ClientCertType'] = "Ref";
233
        }
234
235
        $eaparray["Outer"] = $eapPrettyprint["OUTER"];
236
        if ($eapPrettyprint["INNER"] == "MSCHAPv2") {
237
            $eaparray["Inner"] = $eapPrettyprint["INNER"];
238
        }
239
        $eaparray["SaveCredentials"] = true;
240
        $eaparray["ServerCARefs"] = $caRefs; // maybe takes just one CA?
241
        $eaparray["UseSystemCAs"] = false;
242
243
        if ($outerId !== NULL) {
244
            $eaparray["AnonymousIdentity"] = $outerId;
245
        }
246
        if ($selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
247
            $eaparray["Identity"] = $this->clientCert["certObject"]->username;
248
        }
249
        return $eaparray;
250
    }
251
252
    /**
253
     * prepare a ONC file
254
     *
255
     * @return string installer path name
256
     */
257
    public function writeInstaller() {
258
        $this->loggerInstance->debug(4, "Chromebook Installer start\n");
259
        $caRefs = [];
260
        // we don't do per-user encrypted containers
261
        $jsonArray = ["Type" => "UnencryptedConfiguration"];
262
263
        foreach ($this->attributes['internal:CAs'][0] as $ca) {
264
            $caRefs[] = "{" . $ca['uuid'] . "}";
265
        }
266
        // define CA certificates
267
        foreach ($this->attributes['internal:CAs'][0] as $ca) {
268
            // strip -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----
269
            $this->loggerInstance->debug(3, $ca['pem']);
270
            $caSanitized1 = substr($ca['pem'], 27, strlen($ca['pem']) - 27 - 25 - 1);
271
            if ($caSanitized1 === FALSE) {
272
                throw new Exception("Error cropping PEM data at its BEGIN marker.");
273
            }
274
            $this->loggerInstance->debug(4, $caSanitized1 . "\n");
275
            // remove \n
276
            $caSanitized = str_replace("\n", "", $caSanitized1);
277
            $jsonArray["Certificates"][] = ["GUID" => "{" . $ca['uuid'] . "}", "Remove" => false, "Type" => "Authority", "X509" => $caSanitized];
278
            $this->loggerInstance->debug(3, $caSanitized . "\n");
279
        }
280
        // if we are doing silverbullet, include the unencrypted(!) P12 as a client certificate
281
        if ($this->selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
282
            $jsonArray["Certificates"][] = ["GUID" => "[" . $this->clientCert['GUID'] . "]", "PKCS12" => base64_encode($this->clientCert['certdataclear']), "Remove" => false, "Type" => "Client"];
283
        }
284
        $eaparray = $this->eapBlock($caRefs);
285
        // define Wi-Fi networks
286
        foreach ($this->attributes['internal:SSID'] as $ssid => $cryptolevel) {
287
            $jsonArray["NetworkConfigurations"][] = $this->wifiBlock($ssid, $eaparray);
288
        }
289
        // are we also configuring wired?
290
        if (isset($this->attributes['media:wired'])) {
291
            $jsonArray["NetworkConfigurations"][] = $this->wiredBlock($eaparray);
292
        }
293
294
        $clearJson = json_encode($jsonArray, JSON_PRETTY_PRINT);
295
        $finalJson = $clearJson;
296
        // if we are doing silverbullet we should also encrypt the entire structure(!) with the import password and embed it into a EncryptedConfiguration
297
        if ($this->selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
298
            $finalJson = $this->encryptConfig($clearJson, $this->clientCert['importPassword']);
299
        }
300
301
        file_put_contents('installer_profile', $finalJson);
302
303
        $fileName = $this->installerBasename . '.onc';
304
305
        if (!$this->sign) {
306
            rename("installer_profile", $fileName);
307
            return $fileName;
308
        }
309
310
        // still here? We are signing. That actually can't be - ONC does not
311
        // have the notion of signing
312
        // but if they ever change their mind, we are prepared
313
314
        $outputFromSigning = system($this->sign . " installer_profile '$fileName' > /dev/null");
315
        if ($outputFromSigning === FALSE) {
316
            $this->loggerInstance->debug(2, "Signing the ONC installer $fileName FAILED!\n");
317
        }
318
319
        return $fileName;
320
    }
321
322
    /**
323
     * prepare module desctiption and usage information
324
     * 
325
     * @return string HTML text to be displayed in the information window
326
     */
327
    public function writeDeviceInfo() {
328
        \core\common\Entity::intoThePotatoes();
329
        $out = "<p>";
330
        $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.");
331
        \core\common\Entity::outOfThePotatoes();
332
        return $out;
333
    }
334
335
}
336