/src/dropbear/src/cli-kex.c
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Dropbear - a SSH2 server |
3 | | * |
4 | | * Copyright (c) 2002-2004 Matt Johnston |
5 | | * Copyright (c) 2004 by Mihnea Stoenescu |
6 | | * All rights reserved. |
7 | | * |
8 | | * Permission is hereby granted, free of charge, to any person obtaining a copy |
9 | | * of this software and associated documentation files (the "Software"), to deal |
10 | | * in the Software without restriction, including without limitation the rights |
11 | | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
12 | | * copies of the Software, and to permit persons to whom the Software is |
13 | | * furnished to do so, subject to the following conditions: |
14 | | * |
15 | | * The above copyright notice and this permission notice shall be included in |
16 | | * all copies or substantial portions of the Software. |
17 | | * |
18 | | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
19 | | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
20 | | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
21 | | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
22 | | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
23 | | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
24 | | * SOFTWARE. */ |
25 | | |
26 | | #include "includes.h" |
27 | | #include "session.h" |
28 | | #include "dbutil.h" |
29 | | #include "algo.h" |
30 | | #include "buffer.h" |
31 | | #include "session.h" |
32 | | #include "kex.h" |
33 | | #include "ssh.h" |
34 | | #include "packet.h" |
35 | | #include "bignum.h" |
36 | | #include "dbrandom.h" |
37 | | #include "runopts.h" |
38 | | #include "signkey.h" |
39 | | #include "ecc.h" |
40 | | |
41 | | |
42 | | static void checkhostkey(const unsigned char* keyblob, unsigned int keybloblen); |
43 | 0 | #define MAX_KNOWNHOSTS_LINE 4500 |
44 | | |
45 | 10.2k | void send_msg_kexdh_init() { |
46 | 10.2k | TRACE(("send_msg_kexdh_init()")) |
47 | | |
48 | 10.2k | CHECKCLEARTOWRITE(); |
49 | | |
50 | 10.2k | #if DROPBEAR_FUZZ |
51 | 10.2k | if (fuzz.fuzzing && fuzz.skip_kexmaths) { |
52 | 4.78k | return; |
53 | 4.78k | } |
54 | 5.48k | #endif |
55 | | |
56 | 5.48k | buf_putbyte(ses.writepayload, SSH_MSG_KEXDH_INIT); |
57 | 5.48k | switch (ses.newkeys->algo_kex->mode) { |
58 | 0 | #if DROPBEAR_NORMAL_DH |
59 | 1.17k | case DROPBEAR_KEX_NORMAL_DH: |
60 | 1.17k | if (ses.newkeys->algo_kex != cli_ses.param_kex_algo |
61 | 1.17k | || !cli_ses.dh_param) { |
62 | 1.17k | if (cli_ses.dh_param) { |
63 | 0 | free_kexdh_param(cli_ses.dh_param); |
64 | 0 | } |
65 | 1.17k | cli_ses.dh_param = gen_kexdh_param(); |
66 | 1.17k | } |
67 | 1.17k | buf_putmpint(ses.writepayload, &cli_ses.dh_param->pub); |
68 | 1.17k | break; |
69 | 0 | #endif |
70 | 0 | #if DROPBEAR_ECDH |
71 | 502 | case DROPBEAR_KEX_ECDH: |
72 | 502 | if (ses.newkeys->algo_kex != cli_ses.param_kex_algo |
73 | 502 | || !cli_ses.ecdh_param) { |
74 | 502 | if (cli_ses.ecdh_param) { |
75 | 0 | free_kexecdh_param(cli_ses.ecdh_param); |
76 | 0 | } |
77 | 502 | cli_ses.ecdh_param = gen_kexecdh_param(); |
78 | 502 | } |
79 | 502 | buf_put_ecc_raw_pubkey_string(ses.writepayload, &cli_ses.ecdh_param->key); |
80 | 502 | break; |
81 | 0 | #endif |
82 | 0 | #if DROPBEAR_CURVE25519 |
83 | 3.80k | case DROPBEAR_KEX_CURVE25519: |
84 | 3.80k | if (ses.newkeys->algo_kex != cli_ses.param_kex_algo |
85 | 3.80k | || !cli_ses.curve25519_param) { |
86 | 3.33k | if (cli_ses.curve25519_param) { |
87 | 0 | free_kexcurve25519_param(cli_ses.curve25519_param); |
88 | 0 | } |
89 | 3.33k | cli_ses.curve25519_param = gen_kexcurve25519_param(); |
90 | 3.33k | } |
91 | 3.80k | buf_putstring(ses.writepayload, cli_ses.curve25519_param->pub, CURVE25519_LEN); |
92 | 3.80k | break; |
93 | 5.48k | #endif |
94 | 5.48k | } |
95 | | |
96 | 5.48k | cli_ses.param_kex_algo = ses.newkeys->algo_kex; |
97 | 5.48k | encrypt_packet(); |
98 | 5.48k | } |
99 | | |
100 | | /* Handle a diffie-hellman key exchange reply. */ |
101 | 4.07k | void recv_msg_kexdh_reply() { |
102 | | |
103 | 4.07k | sign_key *hostkey = NULL; |
104 | 4.07k | unsigned int keytype, keybloblen; |
105 | 4.07k | unsigned char* keyblob = NULL; |
106 | | |
107 | 4.07k | TRACE(("enter recv_msg_kexdh_reply")) |
108 | | |
109 | 4.07k | #if DROPBEAR_FUZZ |
110 | 4.07k | if (fuzz.fuzzing && fuzz.skip_kexmaths) { |
111 | 2.16k | return; |
112 | 2.16k | } |
113 | 1.90k | #endif |
114 | | |
115 | 1.90k | if (cli_ses.kex_state != KEXDH_INIT_SENT) { |
116 | 0 | dropbear_exit("Received out-of-order kexdhreply"); |
117 | 0 | } |
118 | 1.90k | keytype = ses.newkeys->algo_hostkey; |
119 | 1.90k | TRACE(("keytype is %d", keytype)) |
120 | | |
121 | 1.90k | hostkey = new_sign_key(); |
122 | 1.90k | keybloblen = buf_getint(ses.payload); |
123 | | |
124 | 1.90k | keyblob = buf_getptr(ses.payload, keybloblen); |
125 | 1.90k | if (!ses.kexstate.donefirstkex) { |
126 | | /* Only makes sense the first time */ |
127 | 0 | checkhostkey(keyblob, keybloblen); |
128 | 0 | } |
129 | | |
130 | 1.90k | if (buf_get_pub_key(ses.payload, hostkey, &keytype) != DROPBEAR_SUCCESS) { |
131 | 314 | TRACE(("failed getting pubkey")) |
132 | 314 | dropbear_exit("Bad KEX packet"); |
133 | 314 | } |
134 | | |
135 | 1.59k | switch (ses.newkeys->algo_kex->mode) { |
136 | 0 | #if DROPBEAR_NORMAL_DH |
137 | 1.00k | case DROPBEAR_KEX_NORMAL_DH: |
138 | 1.00k | { |
139 | 1.00k | DEF_MP_INT(dh_f); |
140 | 1.00k | m_mp_init(&dh_f); |
141 | 1.00k | if (buf_getmpint(ses.payload, &dh_f) != DROPBEAR_SUCCESS) { |
142 | 10 | TRACE(("failed getting mpint")) |
143 | 10 | dropbear_exit("Bad KEX packet"); |
144 | 10 | } |
145 | | |
146 | 991 | kexdh_comb_key(cli_ses.dh_param, &dh_f, hostkey); |
147 | 991 | mp_clear(&dh_f); |
148 | 991 | } |
149 | 0 | break; |
150 | 0 | #endif |
151 | 0 | #if DROPBEAR_ECDH |
152 | 123 | case DROPBEAR_KEX_ECDH: |
153 | 123 | { |
154 | 123 | buffer *ecdh_qs = buf_getstringbuf(ses.payload); |
155 | 123 | kexecdh_comb_key(cli_ses.ecdh_param, ecdh_qs, hostkey); |
156 | 123 | buf_free(ecdh_qs); |
157 | 123 | } |
158 | 123 | break; |
159 | 0 | #endif |
160 | 0 | #if DROPBEAR_CURVE25519 |
161 | 340 | case DROPBEAR_KEX_CURVE25519: |
162 | 340 | { |
163 | 340 | buffer *ecdh_qs = buf_getstringbuf(ses.payload); |
164 | 340 | kexcurve25519_comb_key(cli_ses.curve25519_param, ecdh_qs, hostkey); |
165 | 340 | buf_free(ecdh_qs); |
166 | 340 | } |
167 | 340 | break; |
168 | 1.59k | #endif |
169 | 1.59k | } |
170 | | |
171 | 1.35k | #if DROPBEAR_NORMAL_DH |
172 | 1.35k | if (cli_ses.dh_param) { |
173 | 985 | free_kexdh_param(cli_ses.dh_param); |
174 | 985 | cli_ses.dh_param = NULL; |
175 | 985 | } |
176 | 1.35k | #endif |
177 | 1.35k | #if DROPBEAR_ECDH |
178 | 1.35k | if (cli_ses.ecdh_param) { |
179 | 91 | free_kexecdh_param(cli_ses.ecdh_param); |
180 | 91 | cli_ses.ecdh_param = NULL; |
181 | 91 | } |
182 | 1.35k | #endif |
183 | 1.35k | #if DROPBEAR_CURVE25519 |
184 | 1.35k | if (cli_ses.curve25519_param) { |
185 | 1.35k | free_kexcurve25519_param(cli_ses.curve25519_param); |
186 | 1.35k | cli_ses.curve25519_param = NULL; |
187 | 1.35k | } |
188 | 1.35k | #endif |
189 | | |
190 | 1.35k | cli_ses.param_kex_algo = NULL; |
191 | 1.35k | if (buf_verify(ses.payload, hostkey, ses.newkeys->algo_signature, |
192 | 1.35k | ses.hash) != DROPBEAR_SUCCESS) { |
193 | 910 | dropbear_exit("Bad hostkey signature"); |
194 | 910 | } |
195 | | |
196 | 447 | sign_key_free(hostkey); |
197 | 447 | hostkey = NULL; |
198 | | |
199 | 447 | send_msg_newkeys(); |
200 | 447 | ses.requirenext = SSH_MSG_NEWKEYS; |
201 | 447 | TRACE(("leave recv_msg_kexdh_init")) |
202 | 447 | } |
203 | | |
204 | | static void ask_to_confirm(const unsigned char* keyblob, unsigned int keybloblen, |
205 | 0 | const char* algoname) { |
206 | |
|
207 | 0 | char* fp = NULL; |
208 | 0 | FILE *tty = NULL; |
209 | 0 | int response = 'z'; |
210 | |
|
211 | 0 | fp = sign_key_fingerprint(keyblob, keybloblen); |
212 | |
|
213 | 0 | if (!cli_opts.ask_hostkey) { |
214 | 0 | dropbear_log(LOG_INFO, "\nHost '%s' key unknown.\n(%s fingerprint %s)", |
215 | 0 | cli_opts.remotehost, |
216 | 0 | algoname, |
217 | 0 | fp); |
218 | 0 | dropbear_exit("Not accepted automatically"); |
219 | 0 | } |
220 | | |
221 | 0 | if (cli_opts.always_accept_key) { |
222 | 0 | dropbear_log(LOG_INFO, "\nHost '%s' key accepted unconditionally.\n(%s fingerprint %s)\n", |
223 | 0 | cli_opts.remotehost, |
224 | 0 | algoname, |
225 | 0 | fp); |
226 | 0 | m_free(fp); |
227 | 0 | return; |
228 | 0 | } |
229 | | |
230 | 0 | fprintf(stderr, "\nHost '%s' is not in the trusted hosts file.\n(%s fingerprint %s)\n", |
231 | 0 | cli_opts.remotehost, |
232 | 0 | algoname, |
233 | 0 | fp); |
234 | 0 | m_free(fp); |
235 | 0 | if (cli_opts.batch_mode) { |
236 | 0 | dropbear_exit("Didn't validate host key"); |
237 | 0 | } |
238 | | |
239 | 0 | fprintf(stderr, "Do you want to continue connecting? (y/n) "); |
240 | 0 | tty = fopen(_PATH_TTY, "r"); |
241 | 0 | if (tty) { |
242 | 0 | response = getc(tty); |
243 | 0 | fclose(tty); |
244 | 0 | } else { |
245 | 0 | response = getc(stdin); |
246 | | /* flush stdin buffer */ |
247 | 0 | while ((getchar()) != '\n'); |
248 | 0 | } |
249 | |
|
250 | 0 | if (response == 'y') { |
251 | 0 | return; |
252 | 0 | } |
253 | | |
254 | 0 | dropbear_exit("Didn't validate host key"); |
255 | 0 | } |
256 | | |
257 | | static FILE* open_known_hosts_file(int * readonly) |
258 | 0 | { |
259 | 0 | FILE * hostsfile = NULL; |
260 | 0 | char * filename = NULL; |
261 | 0 | char * homedir = NULL; |
262 | | |
263 | 0 | homedir = getenv("HOME"); |
264 | |
|
265 | 0 | if (!homedir) { |
266 | 0 | struct passwd * pw = NULL; |
267 | 0 | pw = getpwuid(getuid()); |
268 | 0 | if (pw) { |
269 | 0 | homedir = pw->pw_dir; |
270 | 0 | } |
271 | 0 | } |
272 | |
|
273 | 0 | if (homedir) { |
274 | 0 | unsigned int len; |
275 | 0 | len = strlen(homedir); |
276 | 0 | filename = m_malloc(len + 18); /* "/.ssh/known_hosts" and null-terminator*/ |
277 | |
|
278 | 0 | snprintf(filename, len+18, "%s/.ssh", homedir); |
279 | | /* Check that ~/.ssh exists - easiest way is just to mkdir */ |
280 | 0 | if (mkdir(filename, S_IRWXU) != 0) { |
281 | 0 | if (errno != EEXIST) { |
282 | 0 | dropbear_log(LOG_INFO, "Warning: failed creating %s/.ssh: %s", |
283 | 0 | homedir, strerror(errno)); |
284 | 0 | TRACE(("mkdir didn't work: %s", strerror(errno))) |
285 | 0 | goto out; |
286 | 0 | } |
287 | 0 | } |
288 | | |
289 | 0 | snprintf(filename, len+18, "%s/.ssh/known_hosts", homedir); |
290 | 0 | hostsfile = fopen(filename, "a+"); |
291 | | |
292 | 0 | if (hostsfile != NULL) { |
293 | 0 | *readonly = 0; |
294 | 0 | fseek(hostsfile, 0, SEEK_SET); |
295 | 0 | } else { |
296 | | /* We mightn't have been able to open it if it was read-only */ |
297 | 0 | if (errno == EACCES || errno == EROFS) { |
298 | 0 | TRACE(("trying readonly: %s", strerror(errno))) |
299 | 0 | *readonly = 1; |
300 | 0 | hostsfile = fopen(filename, "r"); |
301 | 0 | } |
302 | 0 | } |
303 | 0 | } |
304 | | |
305 | 0 | if (hostsfile == NULL) { |
306 | 0 | TRACE(("hostsfile didn't open: %s", strerror(errno))) |
307 | 0 | dropbear_log(LOG_WARNING, "Failed to open %s/.ssh/known_hosts", |
308 | 0 | homedir); |
309 | 0 | goto out; |
310 | 0 | } |
311 | | |
312 | 0 | out: |
313 | 0 | m_free(filename); |
314 | 0 | return hostsfile; |
315 | 0 | } |
316 | | |
317 | 0 | static void checkhostkey(const unsigned char* keyblob, unsigned int keybloblen) { |
318 | |
|
319 | 0 | FILE *hostsfile = NULL; |
320 | 0 | int readonly = 0; |
321 | 0 | unsigned int hostlen, algolen; |
322 | 0 | unsigned long len; |
323 | 0 | const char *algoname = NULL; |
324 | 0 | char * fingerprint = NULL; |
325 | 0 | buffer * line = NULL; |
326 | 0 | int ret; |
327 | |
|
328 | 0 | if (cli_opts.no_hostkey_check) { |
329 | 0 | dropbear_log(LOG_INFO, "Caution, skipping hostkey check for %s\n", cli_opts.remotehost); |
330 | 0 | return; |
331 | 0 | } |
332 | | |
333 | 0 | algoname = signkey_name_from_type(ses.newkeys->algo_hostkey, &algolen); |
334 | |
|
335 | 0 | hostsfile = open_known_hosts_file(&readonly); |
336 | 0 | if (!hostsfile) { |
337 | 0 | ask_to_confirm(keyblob, keybloblen, algoname); |
338 | | /* ask_to_confirm will exit upon failure */ |
339 | 0 | return; |
340 | 0 | } |
341 | | |
342 | 0 | line = buf_new(MAX_KNOWNHOSTS_LINE); |
343 | 0 | hostlen = strlen(cli_opts.remotehost); |
344 | |
|
345 | 0 | do { |
346 | 0 | if (buf_getline(line, hostsfile) == DROPBEAR_FAILURE) { |
347 | 0 | TRACE(("failed reading line: prob EOF")) |
348 | 0 | break; |
349 | 0 | } |
350 | | |
351 | | /* The line is too short to be sensible */ |
352 | | /* "30" is 'enough to hold ssh-dss plus the spaces, ie so we don't |
353 | | * buf_getfoo() past the end and die horribly - the base64 parsing |
354 | | * code is what tiptoes up to the end nicely */ |
355 | 0 | if (line->len < (hostlen+30) ) { |
356 | 0 | TRACE(("line is too short to be sensible")) |
357 | 0 | continue; |
358 | 0 | } |
359 | | |
360 | | /* Compare hostnames */ |
361 | 0 | if (strncmp(cli_opts.remotehost, (const char *) buf_getptr(line, hostlen), |
362 | 0 | hostlen) != 0) { |
363 | 0 | continue; |
364 | 0 | } |
365 | | |
366 | 0 | buf_incrpos(line, hostlen); |
367 | 0 | if (buf_getbyte(line) != ' ') { |
368 | | /* there wasn't a space after the hostname, something dodgy */ |
369 | 0 | TRACE(("missing space afte matching hostname")) |
370 | 0 | continue; |
371 | 0 | } |
372 | | |
373 | 0 | if (strncmp((const char *) buf_getptr(line, algolen), algoname, algolen) != 0) { |
374 | 0 | TRACE(("algo doesn't match")) |
375 | 0 | continue; |
376 | 0 | } |
377 | | |
378 | 0 | buf_incrpos(line, algolen); |
379 | 0 | if (buf_getbyte(line) != ' ') { |
380 | 0 | TRACE(("missing space after algo")) |
381 | 0 | continue; |
382 | 0 | } |
383 | | |
384 | | /* Now we're at the interesting hostkey */ |
385 | 0 | ret = cmp_base64_key(keyblob, keybloblen, (const unsigned char *) algoname, algolen, |
386 | 0 | line, &fingerprint); |
387 | |
|
388 | 0 | if (ret == DROPBEAR_SUCCESS) { |
389 | | /* Good matching key */ |
390 | 0 | DEBUG1(("server match %s", fingerprint)) |
391 | 0 | goto out; |
392 | 0 | } |
393 | | |
394 | | /* The keys didn't match. eep. Note that we're "leaking" |
395 | | the fingerprint strings here, but we're exiting anyway */ |
396 | 0 | dropbear_exit("\n\n%s host key mismatch for %s !\n" |
397 | 0 | "Fingerprint is %s\n" |
398 | 0 | "Expected %s\n" |
399 | 0 | "If you know that the host key is correct you can\nremove the bad entry from ~/.ssh/known_hosts", |
400 | 0 | algoname, |
401 | 0 | cli_opts.remotehost, |
402 | 0 | sign_key_fingerprint(keyblob, keybloblen), |
403 | 0 | fingerprint ? fingerprint : "UNKNOWN"); |
404 | 0 | } while (1); /* keep going 'til something happens */ |
405 | | |
406 | | /* Key doesn't exist yet */ |
407 | 0 | ask_to_confirm(keyblob, keybloblen, algoname); |
408 | | |
409 | | /* If we get here, they said yes */ |
410 | |
|
411 | 0 | if (readonly) { |
412 | 0 | TRACE(("readonly")) |
413 | 0 | goto out; |
414 | 0 | } |
415 | | |
416 | 0 | if (!cli_opts.no_hostkey_check) { |
417 | | /* put the new entry in the file */ |
418 | 0 | fseek(hostsfile, 0, SEEK_END); /* In case it wasn't opened append */ |
419 | 0 | buf_setpos(line, 0); |
420 | 0 | buf_setlen(line, 0); |
421 | 0 | buf_putbytes(line, (const unsigned char *) cli_opts.remotehost, hostlen); |
422 | 0 | buf_putbyte(line, ' '); |
423 | 0 | buf_putbytes(line, (const unsigned char *) algoname, algolen); |
424 | 0 | buf_putbyte(line, ' '); |
425 | 0 | len = line->size - line->pos; |
426 | | /* The only failure with base64 is buffer_overflow, but buf_getwriteptr |
427 | | * will die horribly in the case anyway */ |
428 | 0 | base64_encode(keyblob, keybloblen, buf_getwriteptr(line, len), &len); |
429 | 0 | buf_incrwritepos(line, len); |
430 | 0 | buf_putbyte(line, '\n'); |
431 | 0 | buf_setpos(line, 0); |
432 | 0 | fwrite(buf_getptr(line, line->len), line->len, 1, hostsfile); |
433 | | /* We ignore errors, since there's not much we can do about them */ |
434 | 0 | } |
435 | |
|
436 | 0 | out: |
437 | 0 | if (hostsfile != NULL) { |
438 | 0 | fclose(hostsfile); |
439 | 0 | } |
440 | 0 | if (line != NULL) { |
441 | 0 | buf_free(line); |
442 | 0 | } |
443 | 0 | m_free(fingerprint); |
444 | 0 | } |
445 | | |
446 | 1.15k | void recv_msg_ext_info(void) { |
447 | | /* This message is not client-specific in the protocol but Dropbear only handles |
448 | | a server-sent message at present. */ |
449 | 1.15k | unsigned int num_ext; |
450 | 1.15k | unsigned int i; |
451 | | |
452 | 1.15k | TRACE(("enter recv_msg_ext_info")) |
453 | | |
454 | | /* Must be after the first SSH_MSG_NEWKEYS */ |
455 | 1.15k | TRACE(("last %d, donefirst %d, donescond %d", ses.lastpacket, ses.kexstate.donefirstkex, ses.kexstate.donesecondkex)) |
456 | 1.15k | if (!(ses.lastpacket == SSH_MSG_NEWKEYS && !ses.kexstate.donesecondkex)) { |
457 | 389 | TRACE(("leave recv_msg_ext_info: ignoring packet received at the wrong time")) |
458 | 389 | return; |
459 | 389 | } |
460 | | |
461 | 768 | num_ext = buf_getint(ses.payload); |
462 | 768 | TRACE(("received SSH_MSG_EXT_INFO with %d items", num_ext)) |
463 | | |
464 | 5.16k | for (i = 0; i < num_ext; i++) { |
465 | 4.39k | unsigned int name_len; |
466 | 4.39k | char *ext_name = buf_getstring(ses.payload, &name_len); |
467 | 4.39k | TRACE(("extension %d name '%s'", i, ext_name)) |
468 | 4.39k | if (cli_ses.server_sig_algs == NULL |
469 | 4.39k | && name_len == strlen(SSH_SERVER_SIG_ALGS) |
470 | 4.39k | && strcmp(ext_name, SSH_SERVER_SIG_ALGS) == 0) { |
471 | 73 | cli_ses.server_sig_algs = buf_getbuf(ses.payload); |
472 | 4.32k | } else { |
473 | | /* valid extension values could be >MAX_STRING_LEN */ |
474 | 4.32k | buf_eatstring(ses.payload); |
475 | 4.32k | } |
476 | 4.39k | m_free(ext_name); |
477 | 4.39k | } |
478 | 768 | TRACE(("leave recv_msg_ext_info")) |
479 | 768 | } |