Skip to main content

sequoia_openpgp/crypto/backend/nettle/
asymmetric.rs

1//! Hold the implementation of [`Signer`] and [`Decryptor`] for [`KeyPair`].
2//!
3//! [`Signer`]: crate::crypto::Signer
4//! [`Decryptor`]: crate::crypto::Decryptor
5//! [`KeyPair`]: crate::crypto::KeyPair
6
7use nettle::{
8    curve25519,
9    curve448,
10    dsa,
11    ecc,
12    ecdh,
13    ecdsa,
14    ed25519,
15    ed448,
16    rsa,
17    random::Yarrow,
18};
19
20use crate::{Error, Result};
21
22use crate::packet::{key, Key};
23use crate::crypto::asymmetric::KeyPair;
24use crate::crypto::backend::interface::Asymmetric;
25use crate::crypto::mpi::{self, MPI, ProtectedMPI, PublicKey};
26use crate::crypto::{
27    SessionKey,
28    mem::Protected,
29};
30use crate::types::{Curve, HashAlgorithm};
31
32impl Asymmetric for super::Backend {
33    fn supports_algo(algo: PublicKeyAlgorithm) -> bool {
34        use PublicKeyAlgorithm::*;
35        #[allow(deprecated)]
36        match algo {
37            X25519 | Ed25519 |
38            RSAEncryptSign | RSAEncrypt | RSASign | DSA | ECDH | ECDSA | EdDSA
39                => true,
40            X448 | Ed448
41                => curve448::IS_SUPPORTED,
42            MLDSA65_Ed25519 => false,
43            MLDSA87_Ed448 => false,
44            SLHDSA128s | SLHDSA128f | SLHDSA256s => false,
45            MLKEM768_X25519 => false,
46            MLKEM1024_X448 => false,
47            ElGamalEncrypt | ElGamalEncryptSign | Private(_) | Unknown(_)
48                => false,
49        }
50    }
51
52    fn supports_curve(curve: &Curve) -> bool {
53        use Curve::*;
54        match curve {
55            NistP256 | NistP384 | NistP521 | Ed25519 | Cv25519
56                => true,
57            BrainpoolP256 | BrainpoolP384 | BrainpoolP512 | Unknown(_)
58                => false,
59        }
60    }
61
62    fn x25519_generate_key() -> Result<(Protected, [u8; 32])> {
63        debug_assert_eq!(curve25519::CURVE25519_SIZE, 32);
64        let mut rng = Yarrow::default();
65        let secret = curve25519::private_key(&mut rng);
66        let mut public = [0; 32];
67        curve25519::mul_g(&mut public, &secret)?;
68        Ok((secret.into(), public))
69    }
70
71    fn x25519_derive_public(secret: &Protected) -> Result<[u8; 32]> {
72        debug_assert_eq!(curve25519::CURVE25519_SIZE, 32);
73        let mut public = [0; 32];
74        curve25519::mul_g(&mut public, secret)?;
75        Ok(public)
76    }
77
78    fn x25519_shared_point(secret: &Protected, public: &[u8; 32])
79                           -> Result<Protected> {
80        debug_assert_eq!(curve25519::CURVE25519_SIZE, 32);
81        let mut s: Protected = vec![0; 32].into();
82        curve25519::mul(&mut s, secret, public)?;
83        Ok(s)
84    }
85
86    fn x448_generate_key() -> Result<(Protected, [u8; 56])> {
87        debug_assert_eq!(curve448::CURVE448_SIZE, 56);
88        if ! curve448::IS_SUPPORTED {
89            return Err(Error::UnsupportedPublicKeyAlgorithm(
90                PublicKeyAlgorithm::Ed448).into());
91        }
92        let mut rng = Yarrow::default();
93        let secret = curve448::private_key(&mut rng);
94        let mut public = [0; 56];
95        curve448::mul_g(&mut public, &secret)?;
96        Ok((secret.into(), public))
97    }
98
99    fn x448_derive_public(secret: &Protected) -> Result<[u8; 56]> {
100        debug_assert_eq!(curve448::CURVE448_SIZE, 56);
101        if ! curve448::IS_SUPPORTED {
102            return Err(Error::UnsupportedPublicKeyAlgorithm(
103                PublicKeyAlgorithm::Ed448).into());
104        }
105        let mut public = [0; 56];
106        curve448::mul_g(&mut public, secret)?;
107        Ok(public)
108    }
109
110    fn x448_shared_point(secret: &Protected, public: &[u8; 56])
111                           -> Result<Protected> {
112        debug_assert_eq!(curve448::CURVE448_SIZE, 56);
113        if ! curve448::IS_SUPPORTED {
114            return Err(Error::UnsupportedPublicKeyAlgorithm(
115                PublicKeyAlgorithm::Ed448).into());
116        }
117        let mut s: Protected = vec![0; 56].into();
118        curve448::mul(&mut s, secret, public)?;
119        Ok(s)
120    }
121
122    fn ed25519_generate_key() -> Result<(Protected, [u8; 32])> {
123        debug_assert_eq!(ed25519::ED25519_KEY_SIZE, 32);
124        let mut rng = Yarrow::default();
125        let mut public = [0; 32];
126        let secret: Protected =
127            ed25519::private_key(&mut rng).into();
128        ed25519::public_key(&mut public, &secret)?;
129        Ok((secret, public))
130    }
131
132    fn ed25519_derive_public(secret: &Protected) -> Result<[u8; 32]> {
133        debug_assert_eq!(ed25519::ED25519_KEY_SIZE, 32);
134        let mut public = [0; 32];
135        ed25519::public_key(&mut public, secret)?;
136        Ok(public)
137    }
138
139    fn ed25519_sign(secret: &Protected, public: &[u8; 32], digest: &[u8])
140                    -> Result<[u8; 64]> {
141        debug_assert_eq!(ed25519::ED25519_KEY_SIZE, 32);
142        debug_assert_eq!(ed25519::ED25519_SIGNATURE_SIZE, 64);
143        let mut sig = [0u8; 64];
144        ed25519::sign(public, secret, digest, &mut sig)?;
145        Ok(sig)
146    }
147
148    fn ed25519_verify(public: &[u8; 32], digest: &[u8], signature: &[u8; 64])
149                      -> Result<bool> {
150        debug_assert_eq!(ed25519::ED25519_KEY_SIZE, 32);
151        debug_assert_eq!(ed25519::ED25519_SIGNATURE_SIZE, 64);
152        Ok(ed25519::verify(public, digest, signature)?)
153    }
154
155    fn ed448_generate_key() -> Result<(Protected, [u8; 57])> {
156        debug_assert_eq!(ed448::ED448_KEY_SIZE, 57);
157        let mut rng = Yarrow::default();
158        let mut public = [0; 57];
159        let secret: Protected =
160            ed448::private_key(&mut rng).into();
161        ed448::public_key(&mut public, &secret)?;
162        Ok((secret, public))
163    }
164
165    fn ed448_derive_public(secret: &Protected) -> Result<[u8; 57]> {
166        debug_assert_eq!(ed448::ED448_KEY_SIZE, 57);
167        let mut public = [0; 57];
168        ed448::public_key(&mut public, secret)?;
169        Ok(public)
170    }
171
172    fn ed448_sign(secret: &Protected, public: &[u8; 57], digest: &[u8])
173                    -> Result<[u8; 114]> {
174        debug_assert_eq!(ed448::ED448_KEY_SIZE, 57);
175        debug_assert_eq!(ed448::ED448_SIGNATURE_SIZE, 114);
176        let mut sig = [0u8; 114];
177        ed448::sign(public, secret, digest, &mut sig)?;
178        Ok(sig)
179    }
180
181    fn ed448_verify(public: &[u8; 57], digest: &[u8], signature: &[u8; 114])
182                      -> Result<bool> {
183        debug_assert_eq!(ed448::ED448_KEY_SIZE, 57);
184        debug_assert_eq!(ed448::ED448_SIGNATURE_SIZE, 114);
185        Ok(ed448::verify(public, digest, signature)?)
186    }
187
188    fn dsa_generate_key(p_bits: usize)
189                        -> Result<(MPI, MPI, MPI, MPI, ProtectedMPI)>
190    {
191        let mut rng = Yarrow::default();
192        let q_bits = if p_bits <= 1024 { 160 } else { 256 };
193        let params = dsa::Params::generate(&mut rng, p_bits, q_bits)?;
194        let (p, q) = params.primes();
195        let g = params.g();
196        let (y, x) = dsa::generate_keypair(&params, &mut rng);
197        Ok((p.into(), q.into(), g.into(), y.as_bytes().into(),
198            x.as_bytes().into()))
199    }
200
201    fn dsa_sign(x: &ProtectedMPI,
202                p: &MPI, q: &MPI, g: &MPI, _y: &MPI,
203                digest: &[u8])
204                -> Result<(MPI, MPI)>
205    {
206        let mut rng = Yarrow::default();
207        let params = dsa::Params::new(p.value(), q.value(), g.value());
208        let secret = dsa::PrivateKey::new(x.value());
209
210        let sig = dsa::sign(&params, &secret, digest, &mut rng)?;
211
212        Ok((MPI::new(&sig.r()), MPI::new(&sig.s())))
213    }
214
215    fn dsa_verify(p: &MPI, q: &MPI, g: &MPI, y: &MPI,
216                  digest: &[u8],
217                  r: &MPI, s: &MPI)
218                  -> Result<bool>
219    {
220        let key = dsa::PublicKey::new(y.value());
221        let params = dsa::Params::new(p.value(), q.value(), g.value());
222        let signature = dsa::Signature::new(r.value(), s.value());
223        Ok(dsa::verify(&params, &key, digest, &signature))
224    }
225}
226
227impl KeyPair {
228    pub(crate) fn sign_backend(&self,
229                               secret: &mpi::SecretKeyMaterial,
230                               hash_algo: HashAlgorithm,
231                               digest: &[u8])
232                               -> Result<mpi::Signature>
233    {
234        use crate::PublicKeyAlgorithm::*;
235
236        let mut rng = Yarrow::default();
237
238        #[allow(deprecated)]
239        match (self.public().pk_algo(), self.public().mpis(), secret)
240        {
241            (RSASign,
242             &PublicKey::RSA { ref e, ref n },
243             &mpi::SecretKeyMaterial::RSA { ref p, ref q, ref d, .. }) |
244            (RSAEncryptSign,
245             &PublicKey::RSA { ref e, ref n },
246             &mpi::SecretKeyMaterial::RSA { ref p, ref q, ref d, .. }) => {
247                let public = rsa::PublicKey::new(n.value(), e.value())?;
248                let secret = rsa::PrivateKey::new(d.value(), p.value(),
249                                                  q.value(), Option::None)?;
250
251                // The signature has the length of the modulus.
252                let mut sig = vec![0u8; n.value().len()];
253
254                // As described in [Section 5.2.2 and 5.2.3 of RFC 9580],
255                // to verify the signature, we need to encode the
256                // signature data in a PKCS1-v1.5 packet.
257                //
258                //   [Section 5.2.2 and 5.2.3 of RFC 9580]:
259                //   https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.2
260                rsa::sign_digest_pkcs1(&public, &secret, digest,
261                                       hash_algo.oid()?,
262                                       &mut rng, &mut sig)?;
263
264                Ok(mpi::Signature::RSA {
265                    s: MPI::new(&sig),
266                })
267            },
268
269            (ECDSA,
270             &PublicKey::ECDSA { ref curve, .. },
271             &mpi::SecretKeyMaterial::ECDSA { ref scalar }) => {
272                let secret = match curve {
273                    Curve::NistP256 =>
274                        ecc::Scalar::new::<ecc::Secp256r1>(
275                            scalar.value())?,
276                    Curve::NistP384 =>
277                        ecc::Scalar::new::<ecc::Secp384r1>(
278                            scalar.value())?,
279                    Curve::NistP521 =>
280                        ecc::Scalar::new::<ecc::Secp521r1>(
281                            scalar.value())?,
282                    _ =>
283                        return Err(
284                            Error::UnsupportedEllipticCurve(curve.clone())
285                                .into()),
286                };
287
288                let sig = ecdsa::sign(&secret, digest, &mut rng);
289
290                Ok(mpi::Signature::ECDSA {
291                    r: MPI::new(&sig.r()),
292                    s: MPI::new(&sig.s()),
293                })
294            },
295
296            (pk_algo, _, _) => Err(Error::InvalidOperation(format!(
297                "unsupported combination of algorithm {:?}, key {:?}, \
298                 and secret key {:?}",
299                pk_algo, self.public(), self.secret())).into()),
300        }
301    }
302}
303
304impl KeyPair {
305    pub(crate) fn decrypt_backend(&self, secret: &mpi::SecretKeyMaterial, ciphertext: &mpi::Ciphertext,
306               plaintext_len: Option<usize>)
307               -> Result<SessionKey>
308    {
309        use crate::PublicKeyAlgorithm::*;
310
311        Ok(match (self.public().mpis(), secret, ciphertext) {
312            (PublicKey::RSA{ ref e, ref n },
313             mpi::SecretKeyMaterial::RSA{ ref p, ref q, ref d, .. },
314             mpi::Ciphertext::RSA{ ref c }) => {
315                let public = rsa::PublicKey::new(n.value(), e.value())?;
316                let secret = rsa::PrivateKey::new(d.value(), p.value(),
317                                                  q.value(), Option::None)?;
318                let mut rand = Yarrow::default();
319                if let Some(l) = plaintext_len {
320                    let mut plaintext: SessionKey = vec![0; l].into();
321                    rsa::decrypt_pkcs1(&public, &secret, &mut rand,
322                                       c.value(), plaintext.as_mut())?;
323                    plaintext
324                } else {
325                    rsa::decrypt_pkcs1_insecure(&public, &secret,
326                                                &mut rand, c.value())?
327                    .into()
328                }
329            }
330
331            (PublicKey::ElGamal{ .. },
332             mpi::SecretKeyMaterial::ElGamal{ .. },
333             mpi::Ciphertext::ElGamal{ .. }) => {
334                #[allow(deprecated)]
335                return Err(
336                    Error::UnsupportedPublicKeyAlgorithm(ElGamalEncrypt).into());
337            },
338
339            (PublicKey::ECDH{ .. },
340             mpi::SecretKeyMaterial::ECDH { .. },
341             mpi::Ciphertext::ECDH { .. }) =>
342                crate::crypto::ecdh::decrypt(self.public(), secret, ciphertext,
343                                             plaintext_len)?,
344
345            (public, secret, ciphertext) =>
346                return Err(Error::InvalidOperation(format!(
347                    "unsupported combination of key pair {:?}/{:?} \
348                     and ciphertext {:?}",
349                    public, secret, ciphertext)).into()),
350        })
351    }
352}
353
354
355impl<P: key::KeyParts, R: key::KeyRole> Key<P, R> {
356    /// Encrypts the given data with this key.
357    pub(crate) fn encrypt_backend(&self, data: &SessionKey) -> Result<mpi::Ciphertext> {
358        use crate::PublicKeyAlgorithm::*;
359
360        #[allow(deprecated)]
361        match self.pk_algo() {
362            RSAEncryptSign | RSAEncrypt => {
363                // Extract the public recipient.
364                match self.mpis() {
365                    mpi::PublicKey::RSA { e, n } => {
366                        // The ciphertext has the length of the modulus.
367                        let ciphertext_len = n.value().len();
368                        if data.len() + 11 > ciphertext_len {
369                            return Err(Error::InvalidArgument(
370                                "Plaintext data too large".into()).into());
371                        }
372
373                        let mut esk = vec![0u8; ciphertext_len];
374                        let mut rng = Yarrow::default();
375                        let pk = rsa::PublicKey::new(n.value(), e.value())?;
376                        rsa::encrypt_pkcs1(&pk, &mut rng, data,
377                                           &mut esk)?;
378                        Ok(mpi::Ciphertext::RSA {
379                            c: MPI::new(&esk),
380                        })
381                    },
382                    pk => {
383                        Err(Error::MalformedPacket(
384                            format!(
385                                "Key: Expected RSA public key, got {:?}",
386                                pk)).into())
387                    },
388                }
389            },
390
391            ECDH => crate::crypto::ecdh::encrypt(self.parts_as_public(),
392                                                 data),
393
394            RSASign | DSA | ECDSA | EdDSA | Ed25519 | Ed448
395                | MLDSA65_Ed25519 | MLDSA87_Ed448
396                | SLHDSA128s | SLHDSA128f | SLHDSA256s =>
397                Err(Error::InvalidOperation(
398                    format!("{} is not an encryption algorithm", self.pk_algo())
399                ).into()),
400
401            X25519 | // Handled in common code.
402            X448 | // Handled in common code.
403            ElGamalEncrypt | ElGamalEncryptSign |
404            MLKEM768_X25519 | // Handled in common code.
405            MLKEM1024_X448 | // Handled in common code.
406            Private(_) | Unknown(_) =>
407                Err(Error::UnsupportedPublicKeyAlgorithm(self.pk_algo()).into()),
408        }
409    }
410
411    /// Verifies the given signature.
412    pub(crate) fn verify_backend(&self, sig: &mpi::Signature, hash_algo: HashAlgorithm,
413                  digest: &[u8]) -> Result<()>
414    {
415        use crate::crypto::mpi::Signature;
416
417        let ok = match (self.mpis(), sig) {
418            (PublicKey::RSA { e, n }, Signature::RSA { s }) => {
419                let key = rsa::PublicKey::new(n.value(), e.value())?;
420
421                // As described in [Section 5.2.2 and 5.2.3 of RFC 9580],
422                // to verify the signature, we need to encode the
423                // signature data in a PKCS1-v1.5 packet.
424                //
425                //   [Section 5.2.2 and 5.2.3 of RFC 9580]:
426                //   https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.2
427                rsa::verify_digest_pkcs1(&key, digest, hash_algo.oid()?,
428                                         s.value())?
429            },
430            (PublicKey::ECDSA { curve, q }, Signature::ECDSA { s, r }) =>
431            {
432                let (x, y) = q.decode_point(curve)?;
433                let key = match curve {
434                    Curve::NistP256 => ecc::Point::new::<ecc::Secp256r1>(x, y)?,
435                    Curve::NistP384 => ecc::Point::new::<ecc::Secp384r1>(x, y)?,
436                    Curve::NistP521 => ecc::Point::new::<ecc::Secp521r1>(x, y)?,
437                    _ => return Err(
438                        Error::UnsupportedEllipticCurve(curve.clone()).into()),
439                };
440
441                let signature = dsa::Signature::new(r.value(), s.value());
442                ecdsa::verify(&key, digest, &signature)
443            },
444            _ => return Err(Error::MalformedPacket(format!(
445                "unsupported combination of key {} and signature {:?}.",
446                self.pk_algo(), sig)).into()),
447        };
448
449        if ok {
450            Ok(())
451        } else {
452            Err(Error::ManipulatedMessage.into())
453        }
454    }
455}
456
457use std::time::SystemTime;
458use crate::packet::key::{Key4, SecretParts};
459use crate::types::PublicKeyAlgorithm;
460
461impl<R> Key4<SecretParts, R>
462    where R: key::KeyRole,
463{
464    /// Creates a new OpenPGP secret key packet for an existing RSA key.
465    ///
466    /// The RSA key will use the secret exponent `d`, derived from the
467    /// secret primes `p` and `q`.  The key will have its creation
468    /// date set to `ctime` or the current time if `None` is given.
469    pub fn import_secret_rsa<T>(d: &[u8], p: &[u8], q: &[u8], ctime: T)
470        -> Result<Self> where T: Into<Option<SystemTime>>
471    {
472        let sec = rsa::PrivateKey::new(d, p, q, None)?;
473        let key = sec.public_key()?;
474        let (a, b, c) = sec.as_rfc4880();
475
476        Self::with_secret(
477            ctime.into().unwrap_or_else(crate::now),
478            PublicKeyAlgorithm::RSAEncryptSign,
479            mpi::PublicKey::RSA {
480                e: mpi::MPI::new(&key.e()[..]),
481                n: mpi::MPI::new(&key.n()[..]),
482            },
483            mpi::SecretKeyMaterial::RSA {
484                d: d.into(),
485                p: a.into(),
486                q: b.into(),
487                u: c.into(),
488            }.into())
489    }
490
491    /// Generates a new RSA key with a public modulus of size `bits`.
492    pub fn generate_rsa(bits: usize) -> Result<Self> {
493        let mut rng = Yarrow::default();
494
495        let (public, private) = rsa::generate_keypair(&mut rng, bits as u32)?;
496        let (p, q, u) = private.as_rfc4880();
497        let public_mpis = PublicKey::RSA {
498            e: MPI::new(&*public.e()),
499            n: MPI::new(&*public.n()),
500        };
501        let private_mpis = mpi::SecretKeyMaterial::RSA {
502            d: private.d().into(),
503            p: p.into(),
504            q: q.into(),
505            u: u.into(),
506        };
507
508        Self::with_secret(
509            crate::now(),
510            PublicKeyAlgorithm::RSAEncryptSign,
511            public_mpis,
512            private_mpis.into())
513    }
514
515    /// Generates a new ECC key over `curve`.
516    ///
517    /// If `for_signing` is false a ECDH key, if it's true either a
518    /// EdDSA or ECDSA key is generated.  Giving `for_signing == true` and
519    /// `curve == Cv25519` will produce an error. Likewise
520    /// `for_signing == false` and `curve == Ed25519` will produce an error.
521    pub(crate) fn generate_ecc_backend(for_signing: bool, curve: Curve)
522                                       -> Result<(PublicKeyAlgorithm,
523                                                  mpi::PublicKey,
524                                                  mpi::SecretKeyMaterial)>
525    {
526        let mut rng = Yarrow::default();
527
528        match (curve.clone(), for_signing) {
529            (Curve::Ed25519, true) =>
530                unreachable!("handled in Key4::generate_ecc"),
531
532            (Curve::Cv25519, false) =>
533                unreachable!("handled in Key4::generate_ecc"),
534
535            (Curve::NistP256, true)  | (Curve::NistP384, true)
536            | (Curve::NistP521, true) => {
537                let (public, private, field_sz) = match curve {
538                    Curve::NistP256 => {
539                        let (pu, sec) =
540                            ecdsa::generate_keypair::<ecc::Secp256r1, _>(&mut rng)?;
541                        (pu, sec, 256)
542                    }
543                    Curve::NistP384 => {
544                        let (pu, sec) =
545                            ecdsa::generate_keypair::<ecc::Secp384r1, _>(&mut rng)?;
546                        (pu, sec, 384)
547                    }
548                    Curve::NistP521 => {
549                        let (pu, sec) =
550                            ecdsa::generate_keypair::<ecc::Secp521r1, _>(&mut rng)?;
551                        (pu, sec, 521)
552                    }
553                    _ => unreachable!(),
554                };
555                let (pub_x, pub_y) = public.as_bytes();
556                let public_mpis =  mpi::PublicKey::ECDSA{
557                    curve,
558                    q: MPI::new_point(&pub_x, &pub_y, field_sz),
559                };
560                let private_mpis = mpi::SecretKeyMaterial::ECDSA{
561                    scalar: private.as_bytes().into(),
562                };
563
564                Ok((PublicKeyAlgorithm::ECDSA, public_mpis, private_mpis))
565            }
566
567            (Curve::NistP256, false)  | (Curve::NistP384, false)
568            | (Curve::NistP521, false) => {
569                    let (private, field_sz) = match curve {
570                        Curve::NistP256 => {
571                            let pv =
572                                ecc::Scalar::new_random::<ecc::Secp256r1, _>(&mut rng);
573
574                            (pv, 256)
575                        }
576                        Curve::NistP384 => {
577                            let pv =
578                                ecc::Scalar::new_random::<ecc::Secp384r1, _>(&mut rng);
579
580                            (pv, 384)
581                        }
582                        Curve::NistP521 => {
583                            let pv =
584                                ecc::Scalar::new_random::<ecc::Secp521r1, _>(&mut rng);
585
586                            (pv, 521)
587                        }
588                        _ => unreachable!(),
589                    };
590                    let public = ecdh::point_mul_g(&private);
591                    let (pub_x, pub_y) = public.as_bytes();
592                    let public_mpis = mpi::PublicKey::ECDH{
593                        q: MPI::new_point(&pub_x, &pub_y, field_sz),
594                        hash:
595                        crate::crypto::ecdh::default_ecdh_kdf_hash(&curve),
596                        sym:
597                        crate::crypto::ecdh::default_ecdh_kek_cipher(&curve),
598                        curve,
599                    };
600                    let private_mpis = mpi::SecretKeyMaterial::ECDH{
601                        scalar: private.as_bytes().into(),
602                    };
603
604                    Ok((PublicKeyAlgorithm::ECDH, public_mpis, private_mpis))
605                }
606
607            _ => Err(Error::UnsupportedEllipticCurve(curve).into()),
608        }
609    }
610}