Added Config class
[memberdb.git] / include / cryptography.php
1 <?php
2
3 /* CRYPTOGRAPHY DOCUMENTATION {{{
4
5 Derived from http://clemens.endorphin.org/TKS1-draft.pdf
6
7 1. Initial setup
8     1.1 Ask for password1
9     1.2 Generate random bytes (salt1)
10     1.3 Generate random bytes (salt2)
11     1.4 Generate random bytes (secret)
12     1.6 Create userkey using PBKDF2 with password1, salt1 (1000 iterations)
13     1.5 Create masterkey using PBKDF2 with secret, salt2 (1000 iterations)
14     1.7 Encrypt masterkey using AES with userkey
15     1.8 Create sha1 hash of masterkey+userkey1+salt1
16     1.9 Store data: key=1.7 salt=1.2, control=1.8
17     
18 2. Generate new key
19     2.1 Ask for password1 (existing)
20     2.2 Ask for password2 (new one)
21     2.3 iterate through valid keys
22         2.3.1 Create userkey1 using PBKDF2 with password1, salt1 (from database) - (1000 iterations)
23         2.3.2 Decrypt key1 (from database) using AES with userkey1
24         2.3.3 Create sha1 hash of masterkey+userkey1+salt1, afterwards compare with control1
25         2.3.4 if equal we have the correct row and the correct master key
26     2.4 Generate random bytes (salt2)
27     2.5 Create userkey2 using PBKDF2 with password2, salt2 (1000 iterations)
28     2.6 Encrypt masterkey (from 2.3) using AES with userkey2
29     2.7 Create sha1 hash of masterkey+userkey2+salt2
30     2.8 Store data: key=2.6 salt=2.4, control=2.7
31
32 3. Delete key
33     3.1 Ask for password
34     3.2 iterate through valid keys
35         3.2.1 Create userkey1 using PBKDF2 with password, salt (from database) - (1000 iterations)
36         3.2.2 Decrypt key (from database) using AES with userkey
37         3.2.3 Create sha1 hash of masterkey+userkey+salt, afterwards compare with control
38         3.2.4 if equal we have the correct row and the correct master key
39         3.2.5 delete the row
40
41 4. Encrypt/Decrypt data
42     4.1 Ask for password
43     4.2 iterate through valid keys
44         4.2.1 Create userkey using PBKDF2 with password, salt (from database) - (1000 iterations)
45         4.2.2 Decrypt key (from database) using AES with userkey
46         4.2.3 Create sha1 hash of masterkey+userkey+salt, afterwards compare with control
47         4.2.4 if equal we have the correct row and the correct master key
48     4.2 Encrypt/Decrypt data using AES with masterkey
49     
50
51 PROBLEM: AES uses a random initialization vector and creates different output for same parameters
52
53 }}} */
54
55
56 $SQL_CREATE_CRYPTO = sprintf('
57     CREATE TABLE IF NOT EXISTS `%1$s` (
58       `description` VARCHAR(255) NOT NULL,
59       `masterkey` BLOB NOT NULL,
60       `salt` BLOB NOT NULL,
61       `control` BLOB NOT NULL,
62       `created_at` DATETIME NOT NULL,
63       `modified_at` DATETIME NOT NULL
64     ) ENGINE=InnoDB DEFAULT CHARSET=%2$s
65     ', DB_TABLE_CRYPTO, 'utf8');
66
67 /* DB functions {{{ */
68 function db_get_crypto_keys()
69 {
70     $sql = 'SELECT * FROM %1$s';
71     return db_select_multi(sprintf($sql, DB_TABLE_CRYPTO));
72 }
73
74 function db_add_crypto_key($key)
75 {
76     $key['created_at'] = db_unixtime2datetime(time());
77     $key['modified_at'] = db_unixtime2datetime(time());
78     return (db_insert_single(DB_TABLE_CRYPTO, $key));
79 }
80
81 /* }}} */
82
83 function validate_crypto_key($userdata, &$dbdata, &$validation)
84 {/*{{{*/
85     $fields = array(
86         'description' => 'string',
87         'masterkey'   => 'string',
88         'salt'        => 'string',
89         'control'     => 'string'
90     );
91     $orig = $dbdata;
92     foreach ($fields as $name => $type) {
93         if (!isset($userdata[$name])) {
94             continue;
95         }
96         $dbdata[$name] = $userdata[$name];
97     }
98     return true;
99 }/*}}}*/
100
101
102 function action_cryptography()
103 {/*{{{*/
104
105     $log_messages = array();
106
107     if (isset($_POST['task'])) {
108         switch ($_POST['task']) {
109             case 'add':
110                 $key = array(
111                     'description' => '',
112                     'masterkey'   => '',
113                     'salt'        => '',
114                     'control'     => ''
115                 );
116                 if (validate_crypto_key($_POST, $key, $log_messages)) {
117                     if (db_add_crypto_key($key)) {
118                         redirect(link_to('cryptography'));
119                     }
120                     echo db_error();
121                 }
122
123                 break;
124             default:
125                 break;
126         }
127     }
128
129     $keys = db_get_crypto_keys();
130
131     ?>
132     <h2>Liste der Schl&uuml;ssel</h2>
133     <?php if (isset($keys)) : ?>
134     <table>
135         <tr>
136             <th>Nickname</th>
137         </tr>
138         <?php foreach ($keys as $key) : ?>
139             <tr>
140                 <td><?= html_escape($key['description']) ?></td>
141             </tr>
142         <?php endforeach ?>
143     </table>
144 <?php else : ?>
145     <p>Bisher gibt's noch keine Schl&uuml;ssel.</p>
146 <?php endif ?>
147     <?php
148
149     form_cryptography($keys, $log_messages);
150 }/*}}}*/
151
152
153 function form_cryptography($keys = array(), $log_messages = array())
154 {/*{{{*/
155     ?>
156     <?php if (empty($keys)) : ?>
157     <div class="clearfix">
158         <noscript>
159             <p class="error">
160                 <strong>Achtung:</strong> Ohne JavaScript geht hier gar nichts. Bitte aktivieren!
161             </p>
162         </noscript>
163         <fieldset class="clearfix">
164             <legend>Ersteinrichtung</legend>
165             <?php log_messages($log_messages); ?>
166             <?= html_text_field('Nickname', 'description_visible') ?>
167             <?= html_password_field('Passwort', 'password1') ?>
168             <?= html_password_field('Passwort (wdh.)', 'password2') ?>
169         </fieldset>
170         <input class="submit" type="button" name="btn_initialize" value="Masterkey erzeugen und speichern"
171                onclick="initialize()"/>
172     </div>
173     <form action="" method="post" id="keyform">
174         <?= html_hidden_field('description') ?>
175         <?= html_hidden_field('masterkey') ?>
176         <?= html_hidden_field('salt') ?>
177         <?= html_hidden_field('control') ?>
178         <?= html_hidden_field('task', 'add') ?>
179     </form>
180
181     <?php js_modal_windows() ?>
182
183     <script type="text/javascript">
184
185         $(document).ready(function () {
186             $('#password2').keypress(function (event) {
187                 if (event.keyCode == '13') {
188                     initialize();
189                 }
190             });
191         });
192
193         function initialize() {/*{{{*/
194 //    var t1 = new Date().getTime()
195
196             var description = $('#description_visible').val();
197             if (description.length < 1) {
198                 alert('Bitte einen Nickname angeben.');
199                 return;
200             }
201
202             // 1.1 Ask for password1
203             // password should be already entered into the two input fields
204             var password1 = $('#password1').val();
205             var password2 = $('#password2').val();
206             if (password1 != password2) {
207                 alert('Die Passw├Ârter sind nicht identisch.');
208                 return;
209             }
210             if (password1.length < 8) {
211                 alert('Das Passwort ist zu kurz (min. 8 Zeichen).');
212                 return;
213             }
214
215             modal_window_show($('#please_wait'));
216             $('#password1').val('');
217             $('#password2').val('');
218
219             setTimeout(function () {
220                 // 1.2 Generate random bytes (salt1)
221                 var salt1 = Crypto.charenc.Binary.bytesToString(Crypto.util.randomBytes(32));
222
223                 // 1.3 Generate random bytes (salt2)
224                 var salt2 = Crypto.charenc.Binary.bytesToString(Crypto.util.randomBytes(32));
225
226                 // 1.4 Generate random bytes (secret)
227                 var secret = Crypto.charenc.Binary.bytesToString(Crypto.util.randomBytes(32));
228
229                 // 1.5 Create masterkey using PBKDF2 with secret, salt2 (1000 iterations)
230                 var masterkey = Crypto.PBKDF2(secret, salt2, 256, {iterations: 1000, asBytes: true});
231                 secret = ''; // we don't need this anymore
232                 salt2 = ''; // we don't need this anymore
233
234                 // 1.6 Create userkey using PBKDF2 with password1, salt1 (1000 iterations)
235                 var userkey = Crypto.PBKDF2(password1, salt1, 256, {iterations: 1000, asBytes: true});
236                 password1 = ''; // we don't need this anymore
237
238                 // 1.7 Encrypt masterkey using AES with userkey
239                 var crypted_masterkey = Crypto.AES.encrypt(masterkey, userkey);
240
241                 // 1.8 Create sha1 hash of masterkey+userkey+salt
242                 var control = Crypto.SHA1(Crypto.charenc.Binary.bytesToString(masterkey) + Crypto.charenc.Binary.bytesToString(userkey) + salt1);
243
244                 /* XXX DEBUG XXX
245                     alert(
246                         "masterkey: " + Crypto.charenc.Binary.bytesToString(masterkey).length + " " + masterkey + "\n" +
247                         "userkey: " + userkey + "\n" +
248                         "control: " + control
249                     );
250                     return;
251                 */
252                 //    var t2 = new Date().getTime()
253                 //    alert((t2 - t1) / 1000); // benchmark
254
255                 // 1.9 Store data: key=1.7 salt=1.2, control=1.8
256                 $('#masterkey').val(crypted_masterkey);
257                 $('#salt').val(Crypto.util.bytesToHex(Crypto.charenc.Binary.stringToBytes(salt1)));
258                 $('#control').val(control);
259                 $('#description').val(description);
260
261                 $('#keyform').submit();
262             }, 500);
263         }
264
265         /*}}}*/
266     </script>
267
268 <?php else : ?>
269
270     <div class="clearfix">
271         <noscript>
272             <p class="error">
273                 <strong>Achtung:</strong> Ohne JavaScript geht hier gar nichts. Bitte aktivieren!
274             </p>
275         </noscript>
276         <fieldset class="clearfix">
277             <legend>Passwort hinzuf&uuml;gen</legend>
278             <?php log_messages($log_messages); ?>
279             <?= html_password_field('Exist. Passwort', 'password1') ?>
280             <small>Bestehendes Passwort</small>
281             <?= html_text_field('Nickname', 'description_visible') ?>
282             <?= html_password_field('Passwort', 'password2') ?>
283             <?= html_password_field('Passwort (wdh.)', 'password3') ?>
284         </fieldset>
285         <input class="submit" type="button" name="btn_add_new_key" value="Passwort hinzuf&uuml;gen"
286                onclick="add_new_key()"/>
287     </div>
288     <form action="" method="post" id="keyform">
289         <?= html_hidden_field('description') ?>
290         <?= html_hidden_field('masterkey') ?>
291         <?= html_hidden_field('salt') ?>
292         <?= html_hidden_field('control') ?>
293         <?= html_hidden_field('task', 'add') ?>
294     </form>
295
296     <?php js_modal_windows() ?>
297     <?php js_get_master_key() ?>
298
299     <script type="text/javascript">
300
301         $(document).ready(function () {
302             $('#password3').keypress(function (event) {
303                 if (event.keyCode == '13') {
304                     add_new_key();
305                 }
306             });
307         });
308
309         function add_new_key() {/*{{{*/
310
311             var description = $('#description_visible').val();
312             if (description.length < 1) {
313                 alert('Bitte einen Nickname angeben.');
314                 return;
315             }
316
317             // 2.1 Check password1 (existing)
318             var password1 = $('#password1').val();
319             if (password1.length < 1) {
320                 alert('Bitte ein existierenges Passwort angeben.');
321                 return;
322             }
323
324             // 2.2 Check password2 (new one)
325             var password2 = $('#password2').val();
326             var password3 = $('#password3').val();
327             if (password2 != password3) {
328                 alert('Die Passw├Ârter sind nicht identisch.');
329                 return;
330             }
331             if (password2.length < 8) {
332                 alert('Das Passwort ist zu kurz (min. 8 Zeichen).');
333                 return;
334             }
335
336             get_master_key(password1, function (masterkey) {
337
338                 $('#password1').val('');
339                 $('#password2').val('');
340                 $('#password3').val('');
341
342                 // 2.4 Generate random bytes (salt2)
343                 var salt2 = Crypto.charenc.Binary.bytesToString(Crypto.util.randomBytes(32));
344
345                 // 2.5 Create userkey2 using PBKDF2 with password2, salt2 (1000 iterations)
346                 var userkey2 = Crypto.PBKDF2(password2, salt2, 256, {iterations: 1000, asBytes: true});
347
348                 // 2.6 Encrypt masterkey (from 2.3) using AES with userkey2
349                 var crypted_masterkey = Crypto.AES.encrypt(masterkey, userkey2);
350
351                 // 2.7 Create sha1 hash of masterkey+userkey2+salt2
352                 var control = Crypto.SHA1(Crypto.charenc.Binary.bytesToString(masterkey) + Crypto.charenc.Binary.bytesToString(userkey2) + salt2);
353
354                 // 2.8 Store data: key=2.6 salt=2.4, control=2.7
355                 $('#masterkey').val(crypted_masterkey);
356                 $('#salt').val(Crypto.util.bytesToHex(Crypto.charenc.Binary.stringToBytes(salt2)));
357                 $('#control').val(control);
358                 $('#description').val(description);
359
360                 $('#keyform').submit();
361             }, 10);
362         }
363
364         /*}}}*/
365     </script>
366 <?php endif ?>
367
368     <?php
369 }/*}}}*/
370
371 function js_get_master_key($keys = null)
372 {/*{{{*/
373     if (!isset($keys)) {
374         $keys = db_get_crypto_keys();
375     }
376     if (empty($keys)) {
377         return;
378     }
379     ?>
380     <script type="text/javascript">
381
382         var keys = new Array(
383             <?=join(",\n", array_map('json_encode', $keys)) ?>
384         );
385
386         function get_master_key(password, callback) {/*{{{*/
387
388             modal_window_show($("#please_wait"));
389
390             // 4.2 iterate through valid keys
391             setTimeout(function () {
392                 check_master_key(0, password, callback);
393             }, 1000);
394
395         }
396
397         /*}}}*/
398
399         function check_master_key(idx, password, callback) {/*{{{*/
400
401             if (idx >= keys.length) {
402                 modal_window_hide();
403                 alert("Falsches Passwort");
404                 return;
405             }
406
407             var crypted_masterkey = keys[idx]['masterkey'];
408             var salt = Crypto.charenc.Binary.bytesToString(Crypto.util.hexToBytes(keys[idx]['salt']));
409             var control1 = keys[idx]['control'];
410
411             // 1 Create userkey using PBKDF2 with password, salt (from database) - (1000 iterations)
412             var userkey = Crypto.PBKDF2(password, salt, 256, {iterations: 1000, asBytes: true});
413
414             // 2 Decrypt key (from database) using AES with userkey
415             var masterkey = Crypto.AES.decrypt(crypted_masterkey, userkey);
416             // 3 Create sha1 hash of masterkey+userkey+salt, afterwards compare with control
417             var control2 = Crypto.SHA1(Crypto.charenc.Binary.bytesToString(masterkey) + Crypto.charenc.Binary.bytesToString(userkey) + salt);
418
419             // 4 if equal we have the correct row and the correct master key
420             if (control1 == control2) {
421                 modal_window_hide();
422                 callback(masterkey);
423                 return;
424             }
425
426             // check next key
427             setTimeout(function () {
428                 check_master_key(idx + 1, password, callback);
429             }, 10);
430
431         }
432
433         /*}}}*/
434
435     </script>
436     <?php
437 }/*}}}*/