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