Passed
Push — master ( 0b1096...5b7f39 )
by Stefan
05:13
created

Device_Chromebook::wiredBlock()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
c 0
b 0
f 0
rs 9.4285
cc 1
eloc 9
nc 1
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($selectedEap, $outerId, $caRefs) {
169
        $eapPrettyprint = \core\common\EAP::eapDisplayName($selectedEap);
170
        // ONC has its own enums, and guess what, they don't always match
171
        if ($eapPrettyprint["INNER"] == "MSCHAPV2") {
172
            $eapPrettyprint["INNER"] = "MSCHAPv2";
173
        }
174
        if ($eapPrettyprint["OUTER"] == "TTLS") {
175
            $eapPrettyprint["OUTER"] = "EAP-TTLS";
176
        }
177
        if ($eapPrettyprint["OUTER"] == "TLS") {
178
            $eapPrettyprint["OUTER"] = "EAP-TLS";
179
        }
180
181
        // define EAP properties
182
183
        $eaparray = [];
184
185
        // if silverbullet, we deliver the client cert inline
186
187
        if ($selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
188
            $eaparray['ClientCertRef'] = "[" . $this->clientCert['GUID'] . "]";
189
            $eaparray['ClientCertType'] = "Ref";
190
        }
191
192
        $eaparray["Outer"] = $eapPrettyprint["OUTER"];
193
        if ($eapPrettyprint["INNER"] == "MSCHAPv2") {
194
            $eaparray["Inner"] = $eapPrettyprint["INNER"];
195
        }
196
        $eaparray["SaveCredentials"] = true;
197
        $eaparray["ServerCARefs"] = $caRefs; // maybe takes just one CA?
198
        $eaparray["UseSystemCAs"] = false;
199
200
        if ($outerId) {
201
            $eaparray["AnonymousIdentity"] = $outerId;
202
        }
203
        if ($selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
204
            $eaparray["Identity"] = $this->clientCert["certObject"]->username;
205
        }
206
        return $eaparray;
207
    }
208
209
    /**
210
     * prepare a ONC file
211
     *
212
     * @return string installer path name
213
     */
214
    public function writeInstaller() {
215
        $this->loggerInstance->debug(4, "Chromebook Installer start\n");
216
        $caRefs = [];
217
        // we don't do per-user encrypted containers
218
        $jsonArray = ["Type" => "UnencryptedConfiguration"];
219
220
        foreach ($this->attributes['internal:CAs'][0] as $ca) {
221
            $caRefs[] = "{" . $ca['uuid'] . "}";
222
        }
223
        // define CA certificates
224
        foreach ($this->attributes['internal:CAs'][0] as $ca) {
225
            // strip -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----
226
            $this->loggerInstance->debug(3, $ca['pem']);
227
            $caSanitized1 = substr($ca['pem'], 27, strlen($ca['pem']) - 27 - 25 - 1);
228
            if ($caSanitized1 === FALSE) {
229
                throw new Exception("Error cropping PEM data at its BEGIN marker.");
230
            }
231
            $this->loggerInstance->debug(4, $caSanitized1 . "\n");
232
            // remove \n
233
            $caSanitized = str_replace("\n", "", $caSanitized1);
234
            $jsonArray["Certificates"][] = ["GUID" => "{" . $ca['uuid'] . "}", "Remove" => false, "Type" => "Authority", "X509" => $caSanitized];
235
            $this->loggerInstance->debug(3, $caSanitized . "\n");
236
        }
237
        // if we are doing silverbullet, include the unencrypted(!) P12 as a client certificate
238
        if ($this->selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
239
            $jsonArray["Certificates"][] = ["GUID" => "[" . $this->clientCert['GUID'] . "]", "PKCS12" => base64_encode($this->clientCert['certdataclear']), "Remove" => false, "Type" => "Client"];
240
        }
241
        $eaparray = $this->eapBlock($this->selectedEap, $this->determineOuterIdString(), $caRefs);
242
        // define Wi-Fi networks
243
        foreach ($this->attributes['internal:SSID'] as $ssid => $cryptolevel) {
244
            $jsonArray["NetworkConfigurations"][] = $this->wifiBlock($ssid, $eaparray);
245
        }
246
        // are we also configuring wired?
247
        if (isset($this->attributes['media:wired'])) {
248
            $jsonArray["NetworkConfigurations"][] = $this->wiredBlock($eaparray);
249
        }
250
251
        $clearJson = json_encode($jsonArray, JSON_PRETTY_PRINT);
252
        $finalJson = $clearJson;
253
        // if we are doing silverbullet we should also encrypt the entire structure(!) with the import password and embed it into a EncryptedConfiguration
254
        if ($this->selectedEap == \core\common\EAP::EAPTYPE_SILVERBULLET) {
255
            $finalJson = $this->encryptConfig($clearJson, $this->clientCert['importPassword']);
256
        }
257
258
        file_put_contents('installer_profile', $finalJson);
259
260
        $fileName = $this->installerBasename . '.onc';
261
262
        if (!$this->sign) {
263
            rename("installer_profile", $fileName);
264
            return $fileName;
265
        }
266
267
        // still here? We are signing. That actually can't be - ONC does not
268
        // have the notion of signing
269
        // but if they ever change their mind, we are prepared
270
271
        $outputFromSigning = system($this->sign . " installer_profile '$fileName' > /dev/null");
272
        if ($outputFromSigning === FALSE) {
273
            $this->loggerInstance->debug(2, "Signing the ONC installer $fileName FAILED!\n");
274
        }
275
276
        return $fileName;
277
    }
278
279
    /**
280
     * prepare module desctiption and usage information
281
     * 
282
     * @return string HTML text to be displayed in the information window
283
     */
284
    public function writeDeviceInfo() {
285
        $out = "<p>";
286
        $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.");
287
        return $out;
288
    }
289
290
}
291