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