Passed
Push — master ( 28eddd...1e0b05 )
by Stefan
08:22
created

Device_Chromebook::eapBlock()   D

Complexity

Conditions 8
Paths 128

Size

Total Lines 41
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 41
rs 4.6666
c 0
b 0
f 0
cc 8
eloc 24
nc 128
nop 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
38
use Exception;
39
40
/**
41
 * This is the main implementation class of the module
42
 *
43
 * The name of the class must the the 'Device_' followed by the name of the module file
44
 * (without the '.php' extension), so in this case the file is "TestModule.php" and
45
 * the class is Device_TestModule.
46
 *
47
 * The class MUST define the constructor method and one additional 
48
 * public method: {@link writeInstaller()}.
49
 *
50
 * All other methods and properties should be private. This example sets zipInstaller method to protected, so that it can be seen in the documentation.
51
 *
52
 * It is important to understand how the device module fits into the whole picture, so here is s short descrption.
53
 * An external caller (for instance {@link GUI::generateInstaller()}) creates the module device instance and prepares
54
 * its environment for a given user profile by calling {@link DeviceConfig::setup()} method.
55
 *      this will:
56
 *       - create the temporary directory and save its path as $this->FPATH
57
 *       - process the CA certificates and store results in $this->attributes['internal:CAs'][0]
58
 *            $this->attributes['internal:CAs'][0] is an array of processed CA certificates
59
 *            a processed certifincate is an array 
60
 *               'pem' points to pem feromat certificate
61
 *               'der' points to der format certificate
62
 *               'md5' points to md5 fingerprint
63
 *               'sha1' points to sha1 fingerprint
64
 *               'name' points to the certificate subject
65
 *               'root' can be 1 for self-signed certificate or 0 otherwise
66
 *       - save the info_file (if exists) and put the name in $this->attributes['internal:info_file_name'][0]
67
 * Finally, the module {@link DeviceConfig::writeInstaller ()} is called and the returned path name is used for user download.
68
 *
69
 * @package ModuleWriting
70
 */
71
class Device_Chromebook extends \core\DeviceConfig {
72
73
    /**
74
     * Number of iterations for the PBKDF2 function. 
75
     * 20000 is the minimum as per ChromeOS ONC spec
76
     * 500000 is the maximum as per Chromium source code
77
     * https://cs.chromium.org/chromium/src/chromeos/network/onc/onc_utils.cc?sq=package:chromium&dr=CSs&rcl=1482394814&l=110
78
     */
79
    const PBKDF2_ITERATIONS = 20000;
80
81
    /**
82
     * Constructs a Device object.
83
     *
84
     * @final not to be redefined
85
     */
86
    final public function __construct() {
87
        parent::__construct();
88
        $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]);
89
    }
90
91
    private function encryptConfig($clearJson, $password) {
92
        $salt = \core\common\Entity::randomString(12);
93
        $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.
94
        $strong = FALSE; // should become TRUE if strong crypto is available like it should.
95
        $initVector = openssl_random_pseudo_bytes(16, $strong);
96
        if ($strong === FALSE) {
97
            $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!");
98
        }
99
        $cryptoJson = openssl_encrypt($clearJson, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $initVector);
100
        $hmac = hash_hmac("sha1", $cryptoJson, $encryptionKey, TRUE);
101
102
        $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));
103
104
        // now, generate the container that holds all the crypto data
105
        $finalArray = [
106
            "Cipher" => "AES256",
107
            "Ciphertext" => base64_encode($cryptoJson),
108
            "HMAC" => base64_encode($hmac), // again by reading source code! And why?
109
            "HMACMethod" => "SHA1",
110
            "Salt" => base64_encode($salt), // this is B64 encoded, but had to read Chromium source code to find out! Not in the spec!
111
            "Stretch" => "PBKDF2",
112
            "Iterations" => Device_Chromebook::PBKDF2_ITERATIONS,
113
            "IV" => base64_encode($initVector),
114
            "Type" => "EncryptedConfiguration",
115
        ];
116
        return json_encode($finalArray);
117
    }
118
119
    private function wifiBlock($ssid, $eapdetails) {
120
        return [
121
            "GUID" => \core\common\Entity::uuid('', $ssid),
122
            "Name" => "$ssid",
123
            "Remove" => false,
124
            "Type" => "WiFi",
125
            "WiFi" => [
126
                "AutoConnect" => true,
127
                "EAP" => $eapdetails,
128
                "HiddenSSID" => false,
129
                "SSID" => $ssid,
130
                "Security" => "WPA-EAP",
131
            ],
132
            "ProxySettings" => $this->proxySettings(),
133
        ];
134
    }
135
136
    protected function proxySettings() {
137
        if (isset($this->attributes['media:force_proxy'])) {
138
            // find the port delimiter. In case of IPv6, there are multiple ':' 
139
            // characters, so we have to find the LAST one
140
            $serverAndPort = explode(':', strrev($this->attributes['media:force_proxy'][0]), 2);
141
            // characters are still reversed, invert on use!
142
            return ["Type" => "Manual",
143
                "Manual" => [
144
                    "SecureHTTPProxy" => [
145
                        "Host" => strrev($serverAndPort[1]),
146
                        "Port" => strrev($serverAndPort[0])
147
                    ]
148
                ]
149
            ];
150
        }
151
        return ["Type" => "WPAD"];
152
    }
153
154
    private function wiredBlock($eapdetails) {
155
        return [
156
            "GUID" => \core\common\Entity::uuid('', "wired-dot1x-ethernet") . "}",
157
            "Name" => "eduroam configuration (wired network)",
158
            "Remove" => false,
159
            "Type" => "Ethernet",
160
            "Ethernet" => [
161
                "Authentication" => "8021X",
162
                "EAP" => $eapdetails,
163
            ],
164
            "ProxySettings" => ["Type" => "WPAD"],
165
        ];
166
    }
167
168
    private function eapBlock($caRefs) {
169
        $selectedEap = $this->selectedEap;
170
        $outerId = $this->determineOuterIdString();
171
        $eapPrettyprint = \core\common\EAP::eapDisplayName($selectedEap);
172
        // ONC has its own enums, and guess what, they don't always match
173
        if ($eapPrettyprint["INNER"] == "MSCHAPV2") {
174
            $eapPrettyprint["INNER"] = "MSCHAPv2";
175
        }
176
        if ($eapPrettyprint["OUTER"] == "TTLS") {
177
            $eapPrettyprint["OUTER"] = "EAP-TTLS";
178
        }
179
        if ($eapPrettyprint["OUTER"] == "TLS") {
180
            $eapPrettyprint["OUTER"] = "EAP-TLS";
181
        }
182
183
        // define EAP properties
184
185
        $eaparray = [];
186
187
        // if silverbullet, we deliver the client cert inline
188
189
        if ($selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
190
            $eaparray['ClientCertRef'] = "[" . $this->clientCert['GUID'] . "]";
191
            $eaparray['ClientCertType'] = "Ref";
192
        }
193
194
        $eaparray["Outer"] = $eapPrettyprint["OUTER"];
195
        if ($eapPrettyprint["INNER"] == "MSCHAPv2") {
196
            $eaparray["Inner"] = $eapPrettyprint["INNER"];
197
        }
198
        $eaparray["SaveCredentials"] = true;
199
        $eaparray["ServerCARefs"] = $caRefs; // maybe takes just one CA?
200
        $eaparray["UseSystemCAs"] = false;
201
202
        if ($outerId !== NULL) {
203
            $eaparray["AnonymousIdentity"] = $outerId;
204
        }
205
        if ($selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
206
            $eaparray["Identity"] = $this->clientCert["certObject"]->username;
207
        }
208
        return $eaparray;
209
    }
210
211
    /**
212
     * prepare a ONC file
213
     *
214
     * @return string installer path name
215
     */
216
    public function writeInstaller() {
217
        $this->loggerInstance->debug(4, "Chromebook Installer start\n");
218
        $caRefs = [];
219
        // we don't do per-user encrypted containers
220
        $jsonArray = ["Type" => "UnencryptedConfiguration"];
221
222
        foreach ($this->attributes['internal:CAs'][0] as $ca) {
223
            $caRefs[] = "{" . $ca['uuid'] . "}";
224
        }
225
        // define CA certificates
226
        foreach ($this->attributes['internal:CAs'][0] as $ca) {
227
            // strip -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----
228
            $this->loggerInstance->debug(3, $ca['pem']);
229
            $caSanitized1 = substr($ca['pem'], 27, strlen($ca['pem']) - 27 - 25 - 1);
230
            if ($caSanitized1 === FALSE) {
231
                throw new Exception("Error cropping PEM data at its BEGIN marker.");
232
            }
233
            $this->loggerInstance->debug(4, $caSanitized1 . "\n");
234
            // remove \n
235
            $caSanitized = str_replace("\n", "", $caSanitized1);
236
            $jsonArray["Certificates"][] = ["GUID" => "{" . $ca['uuid'] . "}", "Remove" => false, "Type" => "Authority", "X509" => $caSanitized];
237
            $this->loggerInstance->debug(3, $caSanitized . "\n");
238
        }
239
        // if we are doing silverbullet, include the unencrypted(!) P12 as a client certificate
240
        if ($this->selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
241
            $jsonArray["Certificates"][] = ["GUID" => "[" . $this->clientCert['GUID'] . "]", "PKCS12" => base64_encode($this->clientCert['certdataclear']), "Remove" => false, "Type" => "Client"];
242
        }
243
        $eaparray = $this->eapBlock($caRefs);
244
        // define Wi-Fi networks
245
        foreach ($this->attributes['internal:SSID'] as $ssid => $cryptolevel) {
246
            $jsonArray["NetworkConfigurations"][] = $this->wifiBlock($ssid, $eaparray);
247
        }
248
        // are we also configuring wired?
249
        if (isset($this->attributes['media:wired'])) {
250
            $jsonArray["NetworkConfigurations"][] = $this->wiredBlock($eaparray);
251
        }
252
253
        $clearJson = json_encode($jsonArray, JSON_PRETTY_PRINT);
254
        $finalJson = $clearJson;
255
        // if we are doing silverbullet we should also encrypt the entire structure(!) with the import password and embed it into a EncryptedConfiguration
256
        if ($this->selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
257
            $finalJson = $this->encryptConfig($clearJson, $this->clientCert['importPassword']);
258
        }
259
260
        file_put_contents('installer_profile', $finalJson);
261
262
        $fileName = $this->installerBasename . '.onc';
263
264
        if (!$this->sign) {
265
            rename("installer_profile", $fileName);
266
            return $fileName;
267
        }
268
269
        // still here? We are signing. That actually can't be - ONC does not
270
        // have the notion of signing
271
        // but if they ever change their mind, we are prepared
272
273
        $outputFromSigning = system($this->sign . " installer_profile '$fileName' > /dev/null");
274
        if ($outputFromSigning === FALSE) {
275
            $this->loggerInstance->debug(2, "Signing the ONC installer $fileName FAILED!\n");
276
        }
277
278
        return $fileName;
279
    }
280
281
    /**
282
     * prepare module desctiption and usage information
283
     * 
284
     * @return string HTML text to be displayed in the information window
285
     */
286
    public function writeDeviceInfo() {
287
        $out = "<p>";
288
        $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.");
289
        return $out;
290
    }
291
292
}
293