Passed
Push — master ( 09cfdd...94b89f )
by Stefan
07:21 queued 03:30
created

Device_Chromebook   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 198
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 24
dl 0
loc 198
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A writeDeviceInfo() 0 4 1
B encryptConfig() 0 26 2
A __construct() 0 3 1
A wifiBlock() 0 14 1
D eapBlock() 0 39 8
C writeInstaller() 0 63 10
A wiredBlock() 0 11 1
1
<?php
2
3
/*
4
 * ******************************************************************************
5
 * Copyright 2011-2017 DANTE Ltd. and GÉANT on behalf of the GN3, GN3+, GN4-1 
6
 * and GN4-2 consortia
7
 *
8
 * License: see the web/copyright.php file in the file structure
9
 * ******************************************************************************
10
 */
11
12
/**
13
 * This file contains the TestModule class
14
 *
15
 * This is a very basic example of using the CAT API.  
16
 *
17
 * The module contains two files
18
 * in the Files directory. They will illustrate the use of the {@link DeviceConfig::copyFile()} method.
19
 * One fille will be coppied without the name change, for the second we will provide a new name.
20
 * The API also contains a similar {@link DeviceConfig::translateFile()} method, which is special to Windows installers and not used in this example.
21
 *
22
 * This module will collect all certificate files stored in the database for a given profile and will copy them to the working directory.
23
 *
24
 * If, for the given profile, an information file is available, this will also be copied to the working directory.
25
 *
26
 * The installer will collect all available configuration attributes and save them to a file in the form of the PHP print_r output.
27
 *
28
 * Finally, the installer will create a zip archive containing all above files and this file 
29
 * will be sent to the user as the configurator file.
30
 *
31
 * Go to the {@link Device_TestModule} and {@link DeviceConfig} class definitions to learn more.
32
 *  
33
 * @package ModuleWriting
34
 */
35
36
namespace devices\chromebook;
37
use Exception;
38
39
/**
40
 * This is the main implementation class of the module
41
 *
42
 * The name of the class must the the 'Device_' followed by the name of the module file
43
 * (without the '.php' extension), so in this case the file is "TestModule.php" and
44
 * the class is Device_TestModule.
45
 *
46
 * The class MUST define the constructor method and one additional 
47
 * public method: {@link writeInstaller()}.
48
 *
49
 * All other methods and properties should be private. This example sets zipInstaller method to protected, so that it can be seen in the documentation.
50
 *
51
 * It is important to understand how the device module fits into the whole picture, so here is s short descrption.
52
 * An external caller (for instance {@link GUI::generateInstaller()}) creates the module device instance and prepares
53
 * its environment for a given user profile by calling {@link DeviceConfig::setup()} method.
54
 *      this will:
55
 *       - create the temporary directory and save its path as $this->FPATH
56
 *       - process the CA certificates and store results in $this->attributes['internal:CAs'][0]
57
 *            $this->attributes['internal:CAs'][0] is an array of processed CA certificates
58
 *            a processed certifincate is an array 
59
 *               'pem' points to pem feromat certificate
60
 *               'der' points to der format certificate
61
 *               'md5' points to md5 fingerprint
62
 *               'sha1' points to sha1 fingerprint
63
 *               'name' points to the certificate subject
64
 *               'root' can be 1 for self-signed certificate or 0 otherwise
65
 *       - save the info_file (if exists) and put the name in $this->attributes['internal:info_file_name'][0]
66
 * Finally, the module {@link DeviceConfig::writeInstaller ()} is called and the returned path name is used for user download.
67
 *
68
 * @package ModuleWriting
69
 */
70
class Device_Chromebook extends \core\DeviceConfig {
71
72
    /**
73
     * Number of iterations for the PBKDF2 function. 
74
     * 20000 is the minimum as per ChromeOS ONC spec
75
     * 500000 is the maximum as per Chromium source code
76
     * https://cs.chromium.org/chromium/src/chromeos/network/onc/onc_utils.cc?sq=package:chromium&dr=CSs&rcl=1482394814&l=110
77
     */
78
    const PBKDF2_ITERATIONS = 20000;
79
80
    /**
81
     * Constructs a Device object.
82
     *
83
     * @final not to be redefined
84
     */
85
    final public function __construct() {
86
        parent::__construct();
87
        $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]);
88
    }
89
90
    private function encryptConfig($clearJson, $password) {
91
        $salt = \core\common\Entity::randomString(12);
92
        $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.
93
        $strong = FALSE; // should become TRUE if strong crypto is available like it should.
94
        $initVector = openssl_random_pseudo_bytes(16, $strong);
95
        if ($strong === FALSE) {
1 ignored issue
show
introduced by
The condition $strong === FALSE can never be false.
Loading history...
96
            $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!");
97
        }
98
        $cryptoJson = openssl_encrypt($clearJson, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $initVector);
99
        $hmac = hash_hmac("sha1", $cryptoJson, $encryptionKey, TRUE);
100
101
        $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));
102
103
        // now, generate the container that holds all the crypto data
104
        $finalArray = [
105
            "Cipher" => "AES256",
106
            "Ciphertext" => base64_encode($cryptoJson),
107
            "HMAC" => base64_encode($hmac), // again by reading source code! And why?
108
            "HMACMethod" => "SHA1",
109
            "Salt" => base64_encode($salt), // this is B64 encoded, but had to read Chromium source code to find out! Not in the spec!
110
            "Stretch" => "PBKDF2",
111
            "Iterations" => Device_Chromebook::PBKDF2_ITERATIONS,
112
            "IV" => base64_encode($initVector),
113
            "Type" => "EncryptedConfiguration",
114
        ];
115
        return json_encode($finalArray);
116
    }
117
118
    private function wifiBlock($ssid, $eapdetails) {
119
        return [
120
                "GUID" => \core\common\Entity::uuid('', $ssid),
121
                "Name" => "$ssid",
122
                "Remove" => false,
123
                "Type" => "WiFi",
124
                "WiFi" => [
125
                    "AutoConnect" => true,
126
                    "EAP" => $eapdetails,
127
                    "HiddenSSID" => false,
128
                    "SSID" => $ssid,
129
                    "Security" => "WPA-EAP",
130
                ],
131
                "ProxySettings" => ["Type" => "WPAD"],
132
            ];
133
    }
134
    
135
    private function wiredBlock($eapdetails) {
136
        return [
137
                "GUID" => \core\common\Entity::uuid('', "wired-dot1x-ethernet") . "}",
138
                "Name" => "eduroam configuration (wired network)",
139
                "Remove" => false,
140
                "Type" => "Ethernet",
141
                "Ethernet" => [
142
                    "Authentication" => "8021X",
143
                    "EAP" => $eapdetails,
144
                ],
145
                "ProxySettings" => ["Type" => "WPAD"],
146
            ];
147
    }
148
    
149
    private function eapBlock($selectedEap, $outerId, $caRefs) {
150
        $eapPrettyprint = \core\common\EAP::eapDisplayName($selectedEap);
151
        // ONC has its own enums, and guess what, they don't always match
152
        if ($eapPrettyprint["INNER"] == "MSCHAPV2") {
153
            $eapPrettyprint["INNER"] = "MSCHAPv2";
154
        }
155
        if ($eapPrettyprint["OUTER"] == "TTLS") {
156
            $eapPrettyprint["OUTER"] = "EAP-TTLS";
157
        }
158
        if ($eapPrettyprint["OUTER"] == "TLS") {
159
            $eapPrettyprint["OUTER"] = "EAP-TLS";
160
        }
161
162
        // define EAP properties
163
164
        $eaparray = [];
165
166
        // if silverbullet, we deliver the client cert inline
167
168
        if ($selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
169
            $eaparray['ClientCertRef'] = "[" . $this->clientCert['GUID'] . "]";
170
            $eaparray['ClientCertType'] = "Ref";
171
        }
172
173
        $eaparray["Outer"] = $eapPrettyprint["OUTER"];
174
        if ($eapPrettyprint["INNER"] == "MSCHAPv2") {
175
            $eaparray["Inner"] = $eapPrettyprint["INNER"];
176
        }
177
        $eaparray["SaveCredentials"] = true;
178
        $eaparray["ServerCARefs"] = $caRefs; // maybe takes just one CA?
179
        $eaparray["UseSystemCAs"] = false;
180
181
        if ($outerId) {
182
            $eaparray["AnonymousIdentity"] = $outerId;
183
        }
184
        if ($selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
185
            $eaparray["Identity"] = $this->clientCert["certObject"]->username;
186
        }
187
        return $eaparray;
188
    }
189
    /**
190
     * prepare a ONC file
191
     *
192
     * @return string installer path name
193
     */
194
    public function writeInstaller() {
195
        $this->loggerInstance->debug(4, "Chromebook Installer start\n");
196
        $caRefs = [];
197
        // we don't do per-user encrypted containers
198
        $jsonArray = ["Type" => "UnencryptedConfiguration"];
199
200
        foreach ($this->attributes['internal:CAs'][0] as $ca) {
201
            $caRefs[] = "{" . $ca['uuid'] . "}";
202
        }
203
        // define CA certificates
204
        foreach ($this->attributes['internal:CAs'][0] as $ca) {
205
            // strip -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----
206
            $this->loggerInstance->debug(3, $ca['pem']);
207
            $caSanitized1 = substr($ca['pem'], 27, strlen($ca['pem']) - 27 - 25 - 1);
208
            if ($caSanitized1 === FALSE) {
209
                throw new Exception("Error cropping PEM data at its BEGIN marker.");
210
            }
211
            $this->loggerInstance->debug(4, $caSanitized1 . "\n");
212
            // remove \n
213
            $caSanitized = str_replace("\n", "", $caSanitized1);
214
            $jsonArray["Certificates"][] = ["GUID" => "{" . $ca['uuid'] . "}", "Remove" => false, "Type" => "Authority", "X509" => $caSanitized];
215
            $this->loggerInstance->debug(3, $caSanitized . "\n");
216
        }
217
        // if we are doing silverbullet, include the unencrypted(!) P12 as a client certificate
218
        if ($this->selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
219
            $jsonArray["Certificates"][] = ["GUID" => "[" . $this->clientCert['GUID'] . "]", "PKCS12" => base64_encode($this->clientCert['certdataclear']), "Remove" => false, "Type" => "Client"];
220
        }
221
        $eaparray = $this->eapBlock($this->selectedEap, $this->determineOuterIdString(), $caRefs);
222
        // define Wi-Fi networks
223
        foreach ($this->attributes['internal:SSID'] as $ssid => $cryptolevel) {
224
            $jsonArray["NetworkConfigurations"][] = $this->wifiBlock($ssid, $eaparray);
225
        }
226
        // are we also configuring wired?
227
        if (isset($this->attributes['media:wired'])) {
228
            $jsonArray["NetworkConfigurations"][] = $this->wiredBlock($eaparray);
229
        }
230
231
        $clearJson = json_encode($jsonArray, JSON_PRETTY_PRINT);
232
        $finalJson = $clearJson;
233
        // if we are doing silverbullet we should also encrypt the entire structure(!) with the import password and embed it into a EncryptedConfiguration
234
        if ($this->selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
235
            $finalJson = $this->encryptConfig($clearJson, $this->clientCert['importPassword']);
236
        }
237
238
        file_put_contents('installer_profile', $finalJson);
239
240
        $fileName = $this->installerBasename . '.onc';
241
242
        if (!$this->sign) {
243
            rename("installer_profile", $fileName);
244
            return $fileName;
245
        }
246
247
        // still here? We are signing. That actually can't be - ONC does not
248
        // have the notion of signing
249
        // but if they ever change their mind, we are prepared
250
251
        $outputFromSigning = system($this->sign . " installer_profile '$fileName' > /dev/null");
252
        if ($outputFromSigning === FALSE) {
1 ignored issue
show
introduced by
The condition $outputFromSigning === FALSE can never be true.
Loading history...
253
            $this->loggerInstance->debug(2, "Signing the ONC installer $fileName FAILED!\n");
254
        }
255
256
        return $fileName;
257
    }
258
259
    /**
260
     * prepare module desctiption and usage information
261
     * 
262
     * @return string HTML text to be displayed in the information window
263
     */
264
    public function writeDeviceInfo() {
265
        $out = "<p>";
266
        $out .= _("This installer is an example only. It produces a zip file containig the IdP certificates, info and logo files (if such have been defined by the IdP administrator) and a dump of all available attributes.");
267
        return $out;
268
    }
269
270
}
271