1use crate::seq::Seq;
23use aes_kw::{KeyInit, KwAes128, KwAes256};
24use ctr::cipher::{KeyIvInit, StreamCipher};
25use std::{fmt, sync::Arc};
26use zeroize::Zeroizing;
27
28const MAX_PERIOD: u32 = 1 << 25;
29const KM_RETRY_US: i64 = 100_000;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum KeySize {
34 Aes128,
36 Aes256,
38}
39impl KeySize {
40 pub(crate) fn len(self) -> usize {
41 match self {
42 Self::Aes128 => 16,
43 Self::Aes256 => 32,
44 }
45 }
46 pub(crate) fn field(self) -> u16 {
47 (self.len() / 8) as u16
48 }
49}
50
51#[derive(Clone)]
53pub struct CryptoConfig {
54 secret: Arc<Zeroizing<Vec<u8>>>,
55 pub key_size: KeySize,
57 pub refresh_packets: u32,
59 pub preannounce_packets: u32,
63}
64impl fmt::Debug for CryptoConfig {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 f.debug_struct("CryptoConfig")
67 .field("secret", &"[REDACTED]")
68 .field("key_size", &self.key_size)
69 .field("refresh_packets", &self.refresh_packets)
70 .field("preannounce_packets", &self.preannounce_packets)
71 .finish()
72 }
73}
74impl CryptoConfig {
75 pub fn new(passphrase: Vec<u8>, key_size: KeySize) -> Result<Self, CryptoError> {
78 let secret = Zeroizing::new(passphrase);
79 if !(10..=79).contains(&secret.len()) || secret.contains(&0) {
80 return Err(CryptoError::Config);
81 }
82 Ok(Self {
83 secret: Arc::new(secret),
84 key_size,
85 refresh_packets: MAX_PERIOD,
86 preannounce_packets: 4000,
87 })
88 }
89 pub(crate) fn valid(&self) -> bool {
90 self.refresh_packets <= MAX_PERIOD
91 && self.preannounce_packets > 0
92 && self.preannounce_packets < self.refresh_packets / 2
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum CryptoError {
99 Config,
101 Random,
103 Malformed,
105 Unsupported,
109 BadSecret,
111 Required,
114 KeyExpired,
117 Replay,
120}
121impl fmt::Display for CryptoError {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 write!(f, "SRT crypto: {self:?}")
124 }
125}
126impl std::error::Error for CryptoError {}
127
128type Secret = Zeroizing<Vec<u8>>;
129struct Material {
130 size: KeySize,
131 salt: [u8; 16],
132 kek: Secret,
133 keys: [Option<Secret>; 2],
134}
135fn index(kk: u8) -> Result<usize, CryptoError> {
136 match kk {
137 1 => Ok(0),
138 2 => Ok(1),
139 _ => Err(CryptoError::Malformed),
140 }
141}
142fn derive(config: &CryptoConfig, salt: &[u8; 16]) -> Secret {
143 let mut key = Zeroizing::new(vec![0; config.key_size.len()]);
144 pbkdf2::pbkdf2_hmac::<sha1::Sha1>(
145 config.secret.as_slice(),
146 salt.get(8..).unwrap_or_default(),
147 2048,
148 &mut key,
149 );
150 key
151}
152fn random_key(size: KeySize) -> Result<Secret, CryptoError> {
153 let mut key = Zeroizing::new(vec![0; size.len()]);
154 getrandom::fill(&mut key).map_err(|_| CryptoError::Random)?;
155 Ok(key)
156}
157fn wrap(size: KeySize, kek: &[u8], plain: &[u8]) -> Result<Vec<u8>, CryptoError> {
158 let mut out = vec![0; plain.len() + 8];
159 match size {
160 KeySize::Aes128 => KwAes128::new_from_slice(kek)
161 .map_err(|_| CryptoError::Config)?
162 .wrap_key(plain, &mut out)
163 .map_err(|_| CryptoError::Malformed)?,
164 KeySize::Aes256 => KwAes256::new_from_slice(kek)
165 .map_err(|_| CryptoError::Config)?
166 .wrap_key(plain, &mut out)
167 .map_err(|_| CryptoError::Malformed)?,
168 };
169 Ok(out)
170}
171fn unwrap(size: KeySize, kek: &[u8], wrapped: &[u8]) -> Result<Secret, CryptoError> {
172 let mut out = Zeroizing::new(vec![
173 0;
174 wrapped
175 .len()
176 .checked_sub(8)
177 .ok_or(CryptoError::Malformed)?
178 ]);
179 match size {
180 KeySize::Aes128 => KwAes128::new_from_slice(kek)
181 .map_err(|_| CryptoError::Config)?
182 .unwrap_key(wrapped, &mut out)
183 .map_err(|_| CryptoError::BadSecret)?,
184 KeySize::Aes256 => KwAes256::new_from_slice(kek)
185 .map_err(|_| CryptoError::Config)?
186 .unwrap_key(wrapped, &mut out)
187 .map_err(|_| CryptoError::BadSecret)?,
188 };
189 Ok(out)
190}
191struct Km<'a> {
192 kk: u8,
193 size: KeySize,
194 salt: [u8; 16],
195 wrapped: &'a [u8],
196}
197impl<'a> Km<'a> {
198 fn parse(bytes: &'a [u8]) -> Result<Self, CryptoError> {
199 let header: [u8; 16] = bytes
200 .get(..16)
201 .ok_or(CryptoError::Malformed)?
202 .try_into()
203 .map_err(|_| CryptoError::Malformed)?;
204 let [
205 v,
206 sign1,
207 sign2,
208 kk,
209 k0,
210 k1,
211 k2,
212 k3,
213 cipher,
214 auth,
215 se,
216 r0,
217 r1,
218 r2,
219 slen,
220 klen,
221 ] = header;
222 if (v, sign1, sign2) != (0x12, 0x20, 0x29)
223 || !(1..=3).contains(&kk)
224 || [r0, r1, r2] != [0; 3]
225 {
226 return Err(CryptoError::Malformed);
227 }
228 if [k0, k1, k2, k3] != [0; 4] || (cipher, auth, se, slen) != (2, 0, 2, 4) {
229 return Err(CryptoError::Unsupported);
230 }
231 let size = match klen {
232 4 => KeySize::Aes128,
233 8 => KeySize::Aes256,
234 _ => return Err(CryptoError::Unsupported),
235 };
236 let count = if kk == 3 { 2 } else { 1 };
237 if bytes.len() != 32 + 8 + count * size.len() {
238 return Err(CryptoError::Malformed);
239 }
240 Ok(Self {
241 kk,
242 size,
243 salt: bytes
244 .get(16..32)
245 .ok_or(CryptoError::Malformed)?
246 .try_into()
247 .map_err(|_| CryptoError::Malformed)?,
248 wrapped: bytes.get(32..).ok_or(CryptoError::Malformed)?,
249 })
250 }
251}
252pub(crate) fn valid_km(bytes: &[u8], field: u16) -> bool {
253 Km::parse(bytes).is_ok_and(|km| field == 0 || km.size.field() == field)
254}
255impl Material {
256 fn message(&self) -> Result<Vec<u8>, CryptoError> {
257 let mut kk = 0;
258 let mut plain = Zeroizing::new(Vec::with_capacity(64));
259 for (i, key) in self.keys.iter().enumerate() {
260 if let Some(key) = key {
261 kk |= 1 << i;
262 plain.extend_from_slice(key);
263 }
264 }
265 let mut out = vec![
266 0x12,
267 0x20,
268 0x29,
269 kk,
270 0,
271 0,
272 0,
273 0,
274 2,
275 0,
276 2,
277 0,
278 0,
279 0,
280 4,
281 (self.size.len() / 4) as u8,
282 ];
283 out.extend_from_slice(&self.salt);
284 out.extend_from_slice(&wrap(self.size, &self.kek, &plain)?);
285 Ok(out)
286 }
287 fn crypt(&self, kk: u8, seq: Seq, bytes: &mut [u8]) -> Result<(), CryptoError> {
288 if bytes.len() > 1456 {
289 return Err(CryptoError::Malformed);
290 }
291 let key = self
292 .keys
293 .get(index(kk)?)
294 .and_then(Option::as_ref)
295 .ok_or(CryptoError::KeyExpired)?;
296 let iv = iv(&self.salt, seq);
297 match self.size {
298 KeySize::Aes128 => ctr::Ctr128BE::<aes::Aes128>::new_from_slices(key, &iv)
299 .map_err(|_| CryptoError::Config)?
300 .apply_keystream(bytes),
301 KeySize::Aes256 => ctr::Ctr128BE::<aes::Aes256>::new_from_slices(key, &iv)
302 .map_err(|_| CryptoError::Config)?
303 .apply_keystream(bytes),
304 }
305 Ok(())
306 }
307}
308fn iv(salt: &[u8; 16], seq: Seq) -> [u8; 16] {
309 let mut out = [0; 16];
310 for (dst, src) in out.iter_mut().take(14).zip(salt) {
311 *dst = *src;
312 }
313 for (dst, src) in out.iter_mut().skip(10).take(4).zip(seq.get().to_be_bytes()) {
314 *dst ^= src;
315 }
316 out
317}
318
319pub(crate) struct Encryptor {
320 material: Material,
321 period: u32,
322 preannounce: u32,
323 count: u32,
324 active: u8,
325 pending: Option<Vec<u8>>,
326 confirmed: bool,
327 next_km: i64,
328}
329impl Encryptor {
330 pub(crate) fn new(config: &CryptoConfig) -> Result<Self, CryptoError> {
331 if !config.valid() {
332 return Err(CryptoError::Config);
333 }
334 let mut salt = [0; 16];
335 getrandom::fill(&mut salt).map_err(|_| CryptoError::Random)?;
336 let material = Material {
337 size: config.key_size,
338 salt,
339 kek: derive(config, &salt),
340 keys: [Some(random_key(config.key_size)?), None],
341 };
342 Ok(Self {
343 material,
344 period: config.refresh_packets,
345 preannounce: config.preannounce_packets,
346 count: 0,
347 active: 1,
348 pending: None,
349 confirmed: false,
350 next_km: 0,
351 })
352 }
353 pub(crate) fn initial(&self) -> Result<Vec<u8>, CryptoError> {
354 self.material.message()
355 }
356 pub(crate) fn confirm_initial(&self, bytes: &[u8]) -> bool {
357 self.initial().is_ok_and(|b| b == bytes)
358 }
359 pub(crate) fn encrypt(
362 &mut self,
363 seq: Seq,
364 bytes: &mut [u8],
365 retained_keys: u8,
366 ) -> Result<u8, CryptoError> {
367 if self.count == self.period {
368 if !self.confirmed {
369 return Err(CryptoError::KeyExpired);
370 }
371 self.active = 3 - self.active;
372 self.count = 0;
373 self.pending = None;
374 self.confirmed = false;
375 }
376 if self.count == self.period - self.preannounce {
377 let other = 3 - self.active;
378 if retained_keys & other != 0 {
379 return Err(CryptoError::KeyExpired);
380 }
381 let slot = self
382 .material
383 .keys
384 .get_mut(index(other)?)
385 .ok_or(CryptoError::Malformed)?;
386 *slot = Some(random_key(self.material.size)?);
387 self.pending = Some(self.material.message()?);
388 self.next_km = 0;
389 }
390 self.material.crypt(self.active, seq, bytes)?;
391 self.count += 1;
392 Ok(self.active)
393 }
394 pub(crate) fn poll_km(&mut self, now: i64) -> Option<Vec<u8>> {
395 if self.confirmed || now < self.next_km {
396 return None;
397 }
398 let bytes = self.pending.as_ref()?.clone();
399 self.next_km = now.saturating_add(KM_RETRY_US);
400 Some(bytes)
401 }
402 pub(crate) fn deadline(&self) -> Option<i64> {
403 (!self.confirmed && self.pending.is_some()).then_some(self.next_km)
404 }
405 pub(crate) fn confirm(&mut self, bytes: &[u8]) -> bool {
406 if self.pending.as_ref().is_some_and(|b| b == bytes) {
407 self.confirmed = true;
408 true
409 } else {
410 false
411 }
412 }
413}
414
415pub(crate) struct Decryptor {
416 material: Material,
417 active: u8,
418 highest: Option<Seq>,
419 transition: Option<Seq>,
420 active_start: Option<Seq>,
421 staged: bool,
422 last_km: Vec<u8>,
423}
424impl Decryptor {
425 pub(crate) fn new(config: &CryptoConfig, bytes: &[u8]) -> Result<Self, CryptoError> {
426 if !config.valid() {
427 return Err(CryptoError::Config);
428 }
429 let km = Km::parse(bytes)?;
430 if km.size != config.key_size {
431 return Err(CryptoError::Unsupported);
432 }
433 let kek = derive(config, &km.salt);
434 let plain = unwrap(km.size, &kek, km.wrapped)?;
435 let mut keys: [Option<Secret>; 2] = [None, None];
436 let mut chunks = plain.chunks_exact(km.size.len());
437 for (i, slot) in keys.iter_mut().enumerate() {
438 if km.kk & (1 << i) != 0 {
439 *slot = Some(Zeroizing::new(
440 chunks.next().ok_or(CryptoError::Malformed)?.to_vec(),
441 ));
442 }
443 }
444 Ok(Self {
445 material: Material {
446 size: km.size,
447 salt: km.salt,
448 kek,
449 keys,
450 },
451 active: if km.kk == 2 { 2 } else { 1 },
452 highest: None,
453 transition: None,
454 active_start: None,
455 staged: km.kk == 3,
456 last_km: bytes.to_vec(),
457 })
458 }
459 pub(crate) fn refresh(&mut self, bytes: &[u8]) -> Result<(), CryptoError> {
460 if bytes == self.last_km {
461 return Ok(());
462 }
463 let km = Km::parse(bytes)?;
464 if km.size != self.material.size || km.salt != self.material.salt {
467 return Err(CryptoError::Unsupported);
468 }
469 if self.staged {
470 return Err(CryptoError::Replay);
471 }
472 let plain = unwrap(km.size, &self.material.kek, km.wrapped)?;
473 let mut chunks = plain.chunks_exact(km.size.len());
474 let mut replacement = None;
475 for (i, slot) in self.material.keys.iter().enumerate() {
476 if km.kk & (1 << i) == 0 {
477 continue;
478 }
479 let key = chunks.next().ok_or(CryptoError::Malformed)?;
480 if 1 << i == self.active {
481 if slot.as_ref().is_none_or(|old| old.as_slice() != key) {
482 return Err(CryptoError::Replay);
483 }
484 } else {
485 if slot.as_ref().is_some_and(|old| old.as_slice() == key) {
486 return Err(CryptoError::Replay);
487 }
488 replacement = Some(Zeroizing::new(key.to_vec()));
489 }
490 }
491 let key = replacement.ok_or(CryptoError::Replay)?;
492 *self
493 .material
494 .keys
495 .get_mut(index(3 - self.active)?)
496 .ok_or(CryptoError::Malformed)? = Some(key);
497 self.staged = true;
498 self.last_km = bytes.to_vec();
499 Ok(())
500 }
501 pub(crate) fn decrypt(
502 &mut self,
503 kk: u8,
504 seq: Seq,
505 bytes: &mut [u8],
506 ) -> Result<(), CryptoError> {
507 index(kk)?;
508 let newer = self.highest.is_none_or(|old| old.distance_to(seq) > 0);
509 if kk != self.active && newer {
510 if !self.staged {
511 return Err(CryptoError::KeyExpired);
512 }
513 self.active = kk;
514 self.staged = false;
515 self.transition = Some(seq);
516 self.active_start = Some(seq);
517 } else if kk != self.active && self.transition.is_some_and(|s| s.distance_to(seq) >= 0) {
518 return Err(CryptoError::Replay);
519 }
520 if kk == self.active {
521 let start = *self.active_start.get_or_insert(seq);
522 if start.distance_to(seq) >= MAX_PERIOD as i32 {
523 return Err(CryptoError::KeyExpired);
524 }
525 }
526 self.material.crypt(kk, seq, bytes)?;
527 if newer {
528 self.highest = Some(seq);
529 }
530 Ok(())
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537 fn hex(s: &str) -> Vec<u8> {
538 s.as_bytes()
539 .chunks_exact(2)
540 .map(|b| {
541 u8::from_str_radix(std::str::from_utf8(b).expect("valid crypto fixture"), 16)
542 .expect("valid crypto fixture")
543 })
544 .collect()
545 }
546 fn config() -> CryptoConfig {
547 CryptoConfig::new(b"test passphrase!".to_vec(), KeySize::Aes128)
548 .expect("valid crypto fixture")
549 }
550 fn fixture() -> Material {
551 let salt = std::array::from_fn(|i| 16 + i as u8);
552 Material {
553 size: KeySize::Aes128,
554 salt,
555 kek: derive(&config(), &salt),
556 keys: [Some(Zeroizing::new((0..16).collect())), None],
557 }
558 }
559 #[test]
560 fn rfc3394_wrap_and_integrity_vector() {
561 let kek = hex("000102030405060708090a0b0c0d0e0f");
562 let key = hex("00112233445566778899aabbccddeeff");
563 let wrapped = hex("1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5");
564 assert_eq!(
565 wrap(KeySize::Aes128, &kek, &key).expect("valid crypto fixture"),
566 wrapped
567 );
568 assert_eq!(
569 *unwrap(KeySize::Aes128, &kek, &wrapped).expect("valid crypto fixture"),
570 key
571 );
572 let mut bad = wrapped;
573 bad[0] ^= 1;
574 assert_eq!(
575 unwrap(KeySize::Aes128, &kek, &bad).err(),
576 Some(CryptoError::BadSecret)
577 );
578 }
579 #[test]
580 fn rfc6070_pbkdf2_vector() {
581 let mut out = [0; 20];
582 pbkdf2::pbkdf2_hmac::<sha1::Sha1>(b"password", b"salt", 2, &mut out);
583 assert_eq!(
584 out.as_slice(),
585 hex("ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957")
586 );
587 }
588 #[test]
589 fn ctr_iv_and_independent_openssl_vector() {
590 let material = fixture();
591 assert_eq!(
592 iv(&material.salt, Seq::new(0x12345678)).as_slice(),
593 hex("10111213141516171819082f4a650000")
594 );
595 let mut plain: Vec<u8> = (0..64).collect();
596 material
597 .crypt(1, Seq::new(0x12345678), &mut plain)
598 .expect("valid crypto fixture");
599 assert_eq!(
601 plain,
602 hex(
603 "8c6ed40552954bfd911b25200a07c0bc28573e7ef3521f5e26310ecb093bdf46e2cb08069963ed815a0d4da78f853892a07b722dea5da6e0d46054ea324790d6"
604 )
605 );
606 material
607 .crypt(1, Seq::new(0x12345678), &mut plain)
608 .expect("valid crypto fixture");
609 assert_eq!(plain, (0..64).collect::<Vec<_>>());
610 }
611 #[test]
612 fn malformed_km_is_bounded_and_wrong_secret_fails() {
613 let km = fixture().message().expect("valid crypto fixture");
614 for n in 0..km.len() {
615 assert!(Decryptor::new(&config(), &km[..n]).is_err());
616 }
617 for i in [0, 1, 2, 3, 4, 8, 9, 10, 11, 12, 13, 14, 15] {
618 let mut bad = km.clone();
619 bad[i] ^= 0x80;
620 assert!(Decryptor::new(&config(), &bad).is_err());
621 }
622 let bad = CryptoConfig::new(b"different secret".to_vec(), KeySize::Aes128)
623 .expect("valid crypto fixture");
624 assert_eq!(
625 Decryptor::new(&bad, &km).err(),
626 Some(CryptoError::BadSecret)
627 );
628 assert!(!format!("{:?}", config()).contains("test passphrase"));
629 }
630 #[test]
631 fn refresh_ack_loss_reorder_replay_and_sequence_wrap() {
632 for size in [KeySize::Aes128, KeySize::Aes256] {
633 let mut cfg = config();
634 cfg.key_size = size;
635 cfg.refresh_packets = 10;
636 cfg.preannounce_packets = 3;
637 let mut tx = Encryptor::new(&cfg).expect("valid crypto fixture");
638 let initial = tx.initial().expect("valid crypto fixture");
639 let mut rx = Decryptor::new(&cfg, &initial).expect("valid crypto fixture");
640 let mut old = None;
641 let mut prior_km = None;
642 for n in 0..34 {
643 let seq = Seq::new(0x7fff_fff8).add(n);
644 let mut data = vec![n as u8; 23];
645 let kk = tx.encrypt(seq, &mut data, 0).expect("valid crypto fixture");
646 if n == 9 {
647 old = Some((kk, seq, data.clone()));
648 }
649 let mut decoded = data;
650 rx.decrypt(kk, seq, &mut decoded)
651 .expect("valid crypto fixture");
652 assert_eq!(decoded, vec![n as u8; 23]);
653 if n == 11 {
654 let (kk, seq, mut data) = old.take().expect("valid crypto fixture");
655 rx.decrypt(kk, seq, &mut data)
656 .expect("valid crypto fixture");
657 assert_eq!(data, vec![9; 23]);
658 }
659 if let Some(km) = tx.poll_km(i64::from(n) * 100_000) {
660 rx.refresh(&km).expect("valid crypto fixture");
661 rx.refresh(&km).expect("valid crypto fixture");
662 if let Some(old) = prior_km.replace(km.clone()) {
663 assert!(rx.refresh(&old).is_err());
664 }
665 assert!(tx.confirm(&km));
666 }
667 }
668 }
669 }
670 #[test]
671 fn unconfirmed_or_retained_key_expiry_fails_closed() {
672 let mut cfg = config();
673 cfg.refresh_packets = 6;
674 cfg.preannounce_packets = 2;
675 let mut tx = Encryptor::new(&cfg).expect("valid crypto fixture");
676 for n in 0..6 {
677 tx.encrypt(Seq::new(n), &mut [1], 0)
678 .expect("valid crypto fixture");
679 }
680 assert_eq!(
681 tx.encrypt(Seq::new(6), &mut [1], 0),
682 Err(CryptoError::KeyExpired)
683 );
684 let mut tx = Encryptor::new(&cfg).expect("valid crypto fixture");
685 for n in 0..4 {
686 tx.encrypt(Seq::new(n), &mut [1], 0)
687 .expect("valid crypto fixture");
688 }
689 assert_eq!(
690 tx.encrypt(Seq::new(4), &mut [1], 2),
691 Err(CryptoError::KeyExpired)
692 );
693 }
694 #[test]
695 fn lost_km_response_is_retried_without_burst_or_key_regeneration() {
696 let mut cfg = config();
697 cfg.refresh_packets = 12;
698 cfg.preannounce_packets = 5;
699 let mut tx = Encryptor::new(&cfg).expect("encryptor");
700 let mut rx = Decryptor::new(&cfg, &tx.initial().expect("initial KM")).expect("decryptor");
701 for n in 0..8 {
702 tx.encrypt(Seq::new(n), &mut [1], 0).expect("encrypt");
703 }
704 let first = tx.poll_km(0).expect("preannounce");
705 rx.refresh(&first).expect("install"); assert!(tx.poll_km(0).is_none());
707 assert!(tx.poll_km(99_999).is_none());
708 let retry = tx.poll_km(100_000).expect("retry");
709 assert_eq!(first, retry);
710 rx.refresh(&retry).expect("duplicate is idempotent");
711 assert!(tx.confirm(&retry));
712 assert!(tx.poll_km(200_000).is_none());
713 for n in 8..13 {
714 tx.encrypt(Seq::new(n), &mut [1], 0)
715 .expect("confirmed rotation");
716 }
717 }
718 #[test]
719 fn independent_kdf_km_header_and_receiver_key_use_limit() {
720 let material = fixture();
721 assert_eq!(
723 material.kek.as_slice(),
724 hex("3090ae37914479967b10bfc92164ab47")
725 );
726 let km = material.message().expect("KM");
727 assert_eq!(&km[..16], hex("12202901000000000200020000000404"));
728 assert_eq!(km.len(), 56);
729 let mut rx = Decryptor::new(&config(), &km).expect("decryptor");
730 rx.decrypt(1, Seq::new(0), &mut [1]).expect("first");
731 assert_eq!(
732 rx.decrypt(1, Seq::new(MAX_PERIOD), &mut [1]),
733 Err(CryptoError::KeyExpired)
734 );
735 }
736}