Test Failed
Push — master ( 83b526...bd639c )
by Stefan
23:38
created

DeviceChromebook::eapBlock()   C

Complexity

Conditions 10
Paths 256

Size

Total Lines 46
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 46
rs 6.1333
cc 10
nc 256
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 DeviceChromebook class, for generating installers
25
 * on ChromeOS.
26
 *
27
 * We follow the specification at: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/onc/docs/onc_spec.md
28
 *  
29
 * @package ModuleWriting
30
 */
31
32
namespace devices\chromebook;
33
34
use Exception;
35
36
/**
37
 * This is the main implementation class of the ChromeOS module
38
 *
39
 * @package ModuleWriting
40
 */
41
class DeviceChromebook extends \core\DeviceConfig
42
{
43
44
    /**
45
     * Number of iterations for the PBKDF2 function. 
46
     * 20000 is the minimum as per ChromeOS ONC spec
47
     * 500000 is the maximum as per Chromium source code
48
     * https://cs.chromium.org/chromium/src/chromeos/network/onc/onc_utils.cc?sq=package:chromium&dr=CSs&rcl=1482394814&l=110
49
     */
50
    const PBKDF2_ITERATIONS = 20000;
51
52
    /**
53
     * Constructs a Device object.
54
     *
55
     * @final not to be redefined
56
     */
57
    final public function __construct()
58
    {
59
        parent::__construct();
60
        $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]);
61
        $this->specialities['media:openroaming'] = _("This device does not support provisioning of OpenRoaming.");
62
        $this->specialities['media:consortium_OI'] = _("This device does not support provisioning of Passpoint networks.");
63
    }
64
65
    /**
66
     * encrypts the entire configuration. Only used in SB to protect the client
67
     * credential
68
     * 
69
     * @param string $clearJson the cleartext JSON string to encrypt
70
     * @param string $password  the import PIN we told the user
71
     * @return string
72
     */
73
    private function encryptConfig($clearJson, $password)
74
    {
75
        $salt = \core\common\Entity::randomString(12);
76
        $encryptionKey = hash_pbkdf2("sha1", $password, $salt, DeviceChromebook::PBKDF2_ITERATIONS, 32, TRUE); // the spec is not clear about the algo. Source code in Chromium makes clear it's SHA1.
77
        $strong = FALSE; // should become TRUE if strong crypto is available like it should.
78
        $initVector = openssl_random_pseudo_bytes(16, $strong);
79
        if ($strong === FALSE) {
80
            $this->loggerInstance->debug(1, "WARNING: OpenSSL reports that a random value was generated with a weak cryptographic algorithm (DeviceChromebook::writeInstaller()). You should investigate the reason for this!");
81
        }
82
        $cryptoJson = openssl_encrypt($clearJson, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $initVector);
83
        $hmac = hash_hmac("sha1", $cryptoJson, $encryptionKey, TRUE);
84
85
        $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));
86
87
        // now, generate the container that holds all the crypto data
88
        $finalArray = [
89
            "Cipher" => "AES256",
90
            "Ciphertext" => base64_encode($cryptoJson),
91
            "HMAC" => base64_encode($hmac), // again by reading source code! And why?
92
            "HMACMethod" => "SHA1",
93
            "Salt" => base64_encode($salt), // this is B64 encoded, but had to read Chromium source code to find out! Not in the spec!
94
            "Stretch" => "PBKDF2",
95
            "Iterations" => DeviceChromebook::PBKDF2_ITERATIONS,
96
            "IV" => base64_encode($initVector),
97
            "Type" => "EncryptedConfiguration",
98
        ];
99
        return json_encode($finalArray);
100
    }
101
102
    /**
103
     * Creates a WiFi block (SSID based only, no support for Passpoint)
104
     * @param string $ssid       the SSID to configure
105
     * @param array  $eapdetails the EAP sub-block as derived from EapBlock()
106
     * @return array
107
     */
108
    private function wifiBlock($ssid, $eapdetails)
109
    {
110
        return [
111
            "GUID" => \core\common\Entity::uuid('', $ssid),
112
            "Name" => "$ssid",
113
            "Remove" => false,
114
            "Type" => "WiFi",
115
            "WiFi" => [
116
                "AutoConnect" => true,
117
                "EAP" => $eapdetails,
118
                "HiddenSSID" => false,
119
                "SSID" => $ssid,
120
                "Security" => "WPA-EAP",
121
            ],
122
            "ProxySettings" => $this->proxySettings(),
123
        ];
124
    }
125
126
    /**
127
     * Creates the ProxySettings block
128
     * 
129
     * @return array
130
     */
131
    protected function proxySettings()
132
    {
133
        if (isset($this->attributes['media:force_proxy'])) {
134
            // find the port delimiter. In case of IPv6, there are multiple ':' 
135
            // characters, so we have to find the LAST one
136
            $serverAndPort = explode(':', strrev($this->attributes['media:force_proxy'][0]), 2);
137
            // characters are still reversed, invert on use!
138
            return ["Type" => "Manual",
139
                "Manual" => [
140
                    "SecureHTTPProxy" => [
141
                        "Host" => strrev($serverAndPort[1]),
142
                        "Port" => strrev($serverAndPort[0])
143
                    ]
144
                ]
145
            ];
146
        }
147
        return ["Type" => "WPAD"];
148
    }
149
150
    /**
151
     * Creates a configuration block for wired Ethernet
152
     * 
153
     * @param array $eapdetails the EAP configuration as created with eapBlock()
154
     * @return array
155
     */
156
    private function wiredBlock($eapdetails)
157
    {
158
        return [
159
            "GUID" => \core\common\Entity::uuid('', "wired-dot1x-ethernet") . "}",
160
            "Name" => "eduroam configuration (wired network)",
161
            "Remove" => false,
162
            "Type" => "Ethernet",
163
            "Ethernet" => [
164
                "Authentication" => "8021X",
165
                "EAP" => $eapdetails,
166
            ],
167
            "ProxySettings" => ["Type" => "WPAD"],
168
        ];
169
    }
170
171
    /**
172
     * Creates the EAP configuration sub-block
173
     * 
174
     * @param array $caRefs list of strings with CA references
175
     * @return array
176
     */
177
    private function eapBlock($caRefs)
178
    {
179
        $selectedEap = $this->selectedEap;
180
        $outerId = $this->determineOuterIdString();
181
        $eapPrettyprint = \core\common\EAP::eapDisplayName($selectedEap);
182
        // ONC has its own enums, and guess what, they don't always match
183
        if ($eapPrettyprint["INNER"] == "MSCHAPV2") {
184
            $eapPrettyprint["INNER"] = "MSCHAPv2";
185
        }
186
        if ($eapPrettyprint["OUTER"] == "TTLS") {
187
            $eapPrettyprint["OUTER"] = "EAP-TTLS";
188
        }
189
        if ($eapPrettyprint["OUTER"] == "TLS") {
190
            $eapPrettyprint["OUTER"] = "EAP-TLS";
191
        }
192
193
        // define EAP properties
194
195
        $eaparray = [];
196
197
        // if silverbullet, we deliver the client cert inline
198
199
        if ($selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
200
            $eaparray['ClientCertRef'] = "[" . $this->clientCert['GUID'] . "]";
201
            $eaparray['ClientCertType'] = "Ref";
202
        }
203
204
        $eaparray["Outer"] = $eapPrettyprint["OUTER"];
205
        if ($eapPrettyprint["INNER"] == "MSCHAPv2" || $eapPrettyprint["INNER"] == "PAP") {
206
            $eaparray["Inner"] = $eapPrettyprint["INNER"];
207
        }
208
        $eaparray["SaveCredentials"] = true;
209
        $eaparray["ServerCARefs"] = $caRefs; // maybe takes just one CA?
210
        $eaparray["UseSystemCAs"] = false;
211
        // enumerate the server names to check against with SubjectAlternativeNameMatch
212
        foreach ($this->attributes['eap:server_name'] as $oneName) {
213
            $eaparray["SubjectAlternativeNameMatch"][] = ["Type" => "DNS", "Value" => $oneName];
214
        }
215
216
        if ($outerId !== NULL) {
217
            $eaparray["AnonymousIdentity"] = $outerId;
218
        }
219
        if ($selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
220
            $eaparray["Identity"] = $this->clientCert["certObject"]->username;
221
        }
222
        return $eaparray;
223
    }
224
225
    /**
226
     * prepare a ONC file
227
     *
228
     * @return string installer path name
229
     * @throws Exception
230
     */
231
    public function writeInstaller()
232
    {
233
        $this->loggerInstance->debug(4, "Chromebook Installer start\n");
234
        $caRefs = [];
235
        // we don't do per-user encrypted containers
236
        $jsonArray = ["Type" => "UnencryptedConfiguration"];
237
238
        foreach ($this->attributes['internal:CAs'][0] as $ca) {
239
            $caRefs[] = "{" . $ca['uuid'] . "}";
240
        }
241
        // define CA certificates
242
        foreach ($this->attributes['internal:CAs'][0] as $ca) {
243
            // strip -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----
244
            $this->loggerInstance->debug(3, $ca['pem']);
245
            $caSanitized1 = substr($ca['pem'], 27, strlen($ca['pem']) - 27 - 25 - 1);
246
            if ($caSanitized1 === FALSE) {
247
                throw new Exception("Error cropping PEM data at its BEGIN marker.");
248
            }
249
            $this->loggerInstance->debug(4, $caSanitized1 . "\n");
250
            // remove \n
251
            $caSanitized = str_replace("\n", "", $caSanitized1);
252
            $jsonArray["Certificates"][] = ["GUID" => "{" . $ca['uuid'] . "}", "Remove" => false, "Type" => "Authority", "X509" => $caSanitized];
253
            $this->loggerInstance->debug(3, $caSanitized . "\n");
254
        }
255
        // if we are doing silverbullet, include the unencrypted(!) P12 as a client certificate
256
        if ($this->selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
257
            $jsonArray["Certificates"][] = ["GUID" => "[" . $this->clientCert['GUID'] . "]", "PKCS12" => base64_encode($this->clientCert['certdataclear']), "Remove" => false, "Type" => "Client"];
258
        }
259
        $eaparray = $this->eapBlock($caRefs);
260
        // define Wi-Fi networks
261
        foreach ($this->attributes['internal:networks'] as $netDefinition) {
262
            foreach ($netDefinition['ssid'] as $ssid) {
263
                $jsonArray["NetworkConfigurations"][] = $this->wifiBlock($ssid, $eaparray);
264
            }
265
        }
266
        // are we also configuring wired?
267
        if (isset($this->attributes['media:wired'])) {
268
            $jsonArray["NetworkConfigurations"][] = $this->wiredBlock($eaparray);
269
        }
270
271
        $clearJson = json_encode($jsonArray, JSON_PRETTY_PRINT);
272
        $finalJson = $clearJson;
273
        // if we are doing silverbullet we should also encrypt the entire structure(!) with the import password and embed it into a EncryptedConfiguration
274
        if ($this->selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
275
            $finalJson = $this->encryptConfig($clearJson, $this->clientCert['importPassword']);
276
        }
277
278
        file_put_contents('installer_profile', $finalJson);
279
280
        $fileName = $this->installerBasename . '.onc';
281
282
        if (!$this->sign) {
283
            rename("installer_profile", $fileName);
284
            return $fileName;
285
        }
286
287
        // still here? We are signing. That actually can't be - ONC does not
288
        // have the notion of signing
289
        // but if they ever change their mind, we are prepared
290
291
        $outputFromSigning = system($this->sign . " installer_profile '$fileName' > /dev/null");
292
        if ($outputFromSigning === FALSE) {
293
            $this->loggerInstance->debug(2, "Signing the ONC installer $fileName FAILED!\n");
294
        }
295
296
        return $fileName;
297
    }
298
299
    /**
300
     * prepare module description and usage information
301
     * 
302
     * @return string HTML text to be displayed in the information window
303
     */
304
    public function writeDeviceInfo()
305
    {
306
        \core\common\Entity::intoThePotatoes();
307
        $out = "<p>";
308
        $out .= _("The installer is a file with the extension '.onc'. Please download it, open Chrome, and navigate to the URL <a href='chrome://network'>chrome://network</a>. Then, use the 'Import ONC file' button. The import is silent; the new network definitions will be added to the preferred networks.");
309
        \core\common\Entity::outOfThePotatoes();
310
        return $out;
311
    }
312
}
313