Skip to main content

acme_proxy/admin/
mfa.rs

1//! The second-factor operations, which both front ends dispatch to and neither
2//! owns.
3//!
4//! The same split [`crate::admin::users`] makes: no printing, no HTTP, no
5//! terminal. `src/cli/webadmin.rs` (`admin user totp …`) and
6//! `src/webadmin/handlers/mfa.rs` both come here, which is what keeps the replay
7//! guard, the session revocation and the "shown once" rule identical between
8//! them.
9//!
10//! The pure halves live next door -- [`crate::admin::totp`] for RFC 6238 and
11//! [`crate::admin::recovery`] for the codes. This file is where they meet a
12//! database.
13
14use std::sync::Arc;
15
16use tracing::{info, warn};
17
18use crate::admin::{password, recovery, totp};
19use crate::sqlite::admin_recovery_code::AdminRecoveryCode;
20use crate::sqlite::admin_session::AdminSession;
21use crate::sqlite::admin_user::AdminUser;
22use crate::sqlite::db::Database;
23use crate::sqlite::nonce::now_secs;
24
25/// Which of the two things an accepted submission was.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum MfaMethod {
28    Totp,
29    RecoveryCode,
30}
31
32impl MfaMethod {
33    /// The `method` field of `admin_mfa_verified`. A literal, so it stays
34    /// greppable.
35    #[must_use]
36    pub fn as_str(self) -> &'static str {
37        match self {
38            MfaMethod::Totp => "totp",
39            MfaMethod::RecoveryCode => "recovery_code",
40        }
41    }
42}
43
44/// The result of checking a second-factor submission.
45///
46/// Every variant but [`MfaOutcome::Accepted`] must reach the client as one
47/// `invalid_credentials`, exactly as [`crate::admin::users::AuthOutcome`]'s
48/// three failures do -- they are kept apart here so the *log* can say which.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum MfaOutcome {
51    Accepted {
52        via: MfaMethod,
53        /// Unspent recovery codes after this submission. The panel shows it;
54        /// a low number is the nudge to regenerate.
55        recovery_codes_left: i64,
56    },
57    /// Neither a valid code for this moment nor an unspent recovery code.
58    Rejected,
59    /// A *correct* TOTP code, resubmitted inside its own 30-second window
60    /// (RFC 6238 §5.2). Refused, and worth its own name: it is what somebody
61    /// replaying an observed code looks like.
62    Replayed,
63    /// No confirmed factor to verify against. Reachable only if a factor was
64    /// removed between the password step and this one.
65    NotEnrolled,
66}
67
68impl MfaOutcome {
69    /// The `reason` field of `admin_mfa_failed`, or `""` when it succeeded.
70    #[must_use]
71    pub fn reason(&self) -> &'static str {
72        match self {
73            MfaOutcome::Accepted { .. } => "",
74            MfaOutcome::Rejected => "wrong_code",
75            MfaOutcome::Replayed => "replayed",
76            MfaOutcome::NotEnrolled => "no_factor",
77        }
78    }
79}
80
81/// Checks a submission against this operator's factor, then against their
82/// recovery codes.
83///
84/// **Order matters.** A TOTP check is three HMACs; a recovery check is up to ten
85/// PBKDF2 runs and a write. The cheap and overwhelmingly common path goes
86/// first, and a submission that cannot be a recovery code by shape never starts
87/// the scan at all.
88///
89/// On success the TOTP path claims its time step
90/// ([`AdminUser::claim_totp_step`]) and the recovery path spends its code
91/// ([`AdminRecoveryCode::consume`]) -- in both cases the database, not this
92/// function, is what makes it single-use.
93pub async fn verify_second_factor(
94    user: &mut AdminUser,
95    submitted: &str,
96    database: Arc<Database>,
97) -> Result<MfaOutcome, sqlx::Error> {
98    let Some(secret) = user.totp_secret.clone() else {
99        return Ok(MfaOutcome::NotEnrolled);
100    };
101
102    let trimmed = submitted.trim();
103    if let Some(step) = totp::verify(&secret, trimmed, now_secs()) {
104        if !user.claim_totp_step(step, &database).await? {
105            return Ok(MfaOutcome::Replayed);
106        }
107        let left = AdminRecoveryCode::count_unused(&user.id, &database).await?;
108        return Ok(MfaOutcome::Accepted {
109            via: MfaMethod::Totp,
110            recovery_codes_left: left,
111        });
112    }
113
114    let candidate = recovery::normalize(trimmed);
115    if !recovery::is_well_formed(&candidate) {
116        return Ok(MfaOutcome::Rejected);
117    }
118
119    for code in AdminRecoveryCode::list_unused(&user.id, &database).await? {
120        match password::verify_password(&code.code_hash, &candidate) {
121            Ok(true) => {
122                if !AdminRecoveryCode::consume(&code.id, &database).await? {
123                    // Lost the race to a concurrent submission of this very
124                    // code. Refusing is the correct answer for the loser.
125                    return Ok(MfaOutcome::Rejected);
126                }
127                let left = AdminRecoveryCode::count_unused(&user.id, &database).await?;
128                warn!(event = "admin_mfa_recovery_code_used",
129                      outcome = "success",
130                      user_id = %user.id,
131                      username = %user.username,
132                      remaining = left);
133                return Ok(MfaOutcome::Accepted {
134                    via: MfaMethod::RecoveryCode,
135                    recovery_codes_left: left,
136                });
137            }
138            Ok(false) => {}
139            Err(error) => {
140                // A corrupt row is not a wrong code -- the same distinction
141                // `users::authenticate` draws. Skip it and say so; the operator
142                // has nine others and no way to guess this from a 401.
143                warn!(event = "admin_recovery_code_hash_unreadable",
144                      outcome = "failure",
145                      user_id = %user.id,
146                      code_id = %code.id,
147                      error = %error);
148            }
149        }
150    }
151
152    Ok(MfaOutcome::Rejected)
153}
154
155/// Mints a secret and stores it as *pending*, returning the two representations
156/// the enrolment page shows exactly once.
157///
158/// Nothing about the operator's current factor changes here: an enrolment that
159/// is started and abandoned leaves a live factor live, which is what makes
160/// "set up a new phone" safe to begin from an authenticated session.
161pub async fn begin_totp_enrolment(
162    user: &mut AdminUser,
163    base_url: &str,
164    database: Arc<Database>,
165) -> Result<totp::Enrolment, sqlx::Error> {
166    let account = totp::account_label(&user.username, base_url);
167    let enrolment = totp::begin_enrolment(totp::ISSUER, &account);
168    user.set_totp_pending(&enrolment.secret, &database).await?;
169    Ok(enrolment)
170}
171
172/// Re-renders an enrolment already begun, or begins one.
173///
174/// A page reload must not invalidate the secret the operator has just scanned
175/// into an app, so the pending bytes are shown again rather than replaced. The
176/// two other representations are derived, not stored -- the database keeps only
177/// the secret.
178pub async fn resume_or_begin_totp_enrolment(
179    user: &mut AdminUser,
180    base_url: &str,
181    database: Arc<Database>,
182) -> Result<totp::Enrolment, sqlx::Error> {
183    let Some(secret) = user.totp_pending_secret.clone() else {
184        return begin_totp_enrolment(user, base_url, database).await;
185    };
186
187    let account = totp::account_label(&user.username, base_url);
188    let secret_base32 = totp::base32_encode(&secret);
189    let uri = totp::provisioning_uri(&secret_base32, totp::ISSUER, &account);
190    Ok(totp::Enrolment {
191        secret,
192        secret_base32,
193        uri,
194    })
195}
196
197/// Confirms a pending enrolment against a code the authenticator produced.
198///
199/// On success: the pending secret becomes the real one, a fresh recovery set is
200/// issued and returned **once**, and every other session this operator holds is
201/// revoked. `None` is a wrong code, and leaves `totp_secret` exactly as it was.
202///
203/// `keep_session` is the token hash of the session doing the enrolling, so the
204/// operator is not logged out by their own action; `None` revokes every session,
205/// which is what a change made on their behalf should do.
206pub async fn confirm_totp_enrolment(
207    user: &mut AdminUser,
208    code: &str,
209    keep_session: Option<&str>,
210    database: Arc<Database>,
211) -> Result<Option<Vec<String>>, sqlx::Error> {
212    let Some(pending) = user.totp_pending_secret.clone() else {
213        return Ok(None);
214    };
215
216    let Some(step) = totp::verify(&pending, code.trim(), now_secs()) else {
217        return Ok(None);
218    };
219
220    user.confirm_totp(&database).await?;
221    // The code that proved the enrolment must not also work as the first login
222    // code. `confirm_totp` cleared the guard, so this is the first claim.
223    user.claim_totp_step(step, &database).await?;
224
225    let codes = issue_recovery_codes(user, database.clone()).await?;
226    revoke_other_sessions(user, keep_session, database).await?;
227
228    info!(event = "admin_mfa_enabled",
229          outcome = "success",
230          user_id = %user.id,
231          username = %user.username,
232          recovery_codes = codes.len());
233    Ok(Some(codes))
234}
235
236/// Removes the factor, any half-finished enrolment, the replay guard and every
237/// recovery code -- a code that recovers access to a factor that no longer
238/// exists is just a second password.
239///
240/// Revokes sessions on the same rule as [`confirm_totp_enrolment`]: a security
241/// control removed that left every other browser signed in would be a change in
242/// name only, which is `users::set_password`'s argument verbatim.
243pub async fn disable_totp(
244    user: &mut AdminUser,
245    keep_session: Option<&str>,
246    database: Arc<Database>,
247) -> Result<(), sqlx::Error> {
248    user.clear_totp(&database).await?;
249    AdminRecoveryCode::delete_for_user(&user.id, &database).await?;
250    revoke_other_sessions(user, keep_session, database).await?;
251
252    info!(event = "admin_mfa_disabled", outcome = "success", user_id = %user.id, username = %user.username);
253    Ok(())
254}
255
256/// Mints a fresh recovery set, superseding the previous one, and returns it
257/// **once**. Nothing stores or logs the plaintext.
258pub async fn regenerate_recovery_codes(
259    user: &AdminUser,
260    database: Arc<Database>,
261) -> Result<Vec<String>, sqlx::Error> {
262    let codes = issue_recovery_codes(user, database).await?;
263    info!(event = "admin_mfa_recovery_codes_regenerated",
264          outcome = "success",
265          user_id = %user.id,
266          username = %user.username,
267          minted = codes.len());
268    Ok(codes)
269}
270
271/// How many unspent recovery codes this operator holds.
272pub async fn recovery_codes_remaining(
273    user_id: &str,
274    database: Arc<Database>,
275) -> Result<i64, sqlx::Error> {
276    AdminRecoveryCode::count_unused(user_id, &database).await
277}
278
279/// How many operators have no confirmed factor -- what the startup warning
280/// under `admin.require_mfa` counts.
281pub async fn operators_without_a_factor(database: Arc<Database>) -> Result<usize, sqlx::Error> {
282    Ok(AdminUser::list_all(&database)
283        .await?
284        .iter()
285        .filter(|user| !user.has_totp())
286        .count())
287}
288
289async fn issue_recovery_codes(
290    user: &AdminUser,
291    database: Arc<Database>,
292) -> Result<Vec<String>, sqlx::Error> {
293    let codes = recovery::generate_codes();
294    // Stored the way they are compared: normalised, so the grouping an operator
295    // reads off the screen never has to survive into the hash.
296    let hashes: Vec<String> = codes
297        .iter()
298        .map(|code| password::hash_generated_secret(&recovery::normalize(code)))
299        .collect();
300    AdminRecoveryCode::replace_all(&user.id, &hashes, &database).await?;
301    Ok(codes)
302}
303
304async fn revoke_other_sessions(
305    user: &AdminUser,
306    keep_session: Option<&str>,
307    database: Arc<Database>,
308) -> Result<u64, sqlx::Error> {
309    match keep_session {
310        Some(token_hash) => {
311            AdminSession::delete_for_user_except(&user.id, token_hash, &database).await
312        }
313        None => AdminSession::delete_for_user(&user.id, &database).await,
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use crate::admin::totp::{DIGITS, step_at, totp_at};
321    use crate::sqlite::admin_session::NewSession;
322
323    async fn db() -> Arc<Database> {
324        Arc::new(Database::connect_in_memory().await.unwrap())
325    }
326
327    async fn operator(database: Arc<Database>) -> AdminUser {
328        AdminUser::create("alice", "hash", &database).await.unwrap()
329    }
330
331    /// An operator with a confirmed factor, returning the secret so a test can
332    /// compute codes against it.
333    async fn enrolled(database: Arc<Database>) -> (AdminUser, Vec<u8>) {
334        let mut user = operator(database.clone()).await;
335        let enrolment = begin_totp_enrolment(&mut user, "http://localhost:3001", database.clone())
336            .await
337            .unwrap();
338        let code = totp_at(&enrolment.secret, step_at(now_secs()), DIGITS);
339        let codes = confirm_totp_enrolment(&mut user, &code, None, database)
340            .await
341            .unwrap()
342            .expect("a freshly generated code must confirm its own enrolment");
343        assert_eq!(codes.len(), recovery::CODE_COUNT);
344        (user, enrolment.secret)
345    }
346
347    #[tokio::test]
348    async fn an_operator_with_no_factor_is_not_enrolled() {
349        let db = db().await;
350        let mut user = operator(db.clone()).await;
351
352        assert_eq!(
353            verify_second_factor(&mut user, "123456", db).await.unwrap(),
354            MfaOutcome::NotEnrolled
355        );
356    }
357
358    #[tokio::test]
359    async fn enrolment_is_two_steps_and_a_wrong_code_finishes_neither() {
360        let db = db().await;
361        let mut user = operator(db.clone()).await;
362
363        let enrolment = begin_totp_enrolment(&mut user, "http://localhost:3001", db.clone())
364            .await
365            .unwrap();
366        assert!(
367            !user.has_totp(),
368            "a pending enrolment is not a second factor"
369        );
370        assert!(user.has_pending_totp());
371
372        // A wrong code leaves the pending secret pending and mints no codes.
373        assert!(
374            confirm_totp_enrolment(&mut user, "000000", None, db.clone())
375                .await
376                .unwrap()
377                .is_none()
378        );
379        assert!(!user.has_totp());
380        assert!(user.has_pending_totp());
381        assert_eq!(
382            recovery_codes_remaining(&user.id, db.clone())
383                .await
384                .unwrap(),
385            0
386        );
387
388        let code = totp_at(&enrolment.secret, step_at(now_secs()), DIGITS);
389        let codes = confirm_totp_enrolment(&mut user, &code, None, db.clone())
390            .await
391            .unwrap()
392            .unwrap();
393
394        assert!(user.has_totp());
395        assert!(
396            !user.has_pending_totp(),
397            "confirming must clear the pending column, not leave two secrets live"
398        );
399        assert_eq!(codes.len(), recovery::CODE_COUNT);
400        assert_eq!(
401            recovery_codes_remaining(&user.id, db).await.unwrap(),
402            recovery::CODE_COUNT as i64
403        );
404    }
405
406    /// RFC 6238 §5.2. The code that proved the enrolment must not also be the
407    /// first login code, and no code may be spent twice inside its window.
408    #[tokio::test]
409    async fn a_correct_code_is_accepted_once_and_replayed_thereafter() {
410        let db = db().await;
411        let (mut user, secret) = enrolled(db.clone()).await;
412
413        // The enrolment already claimed a step, so the code for *that* step is
414        // a replay -- which is the point. Read it back rather than recomputing
415        // it from the clock: the two would disagree whenever the enrolment and
416        // this line straddle a 30-second boundary.
417        let claimed = user.totp_last_step.expect("enrolment claims its own step");
418        let code = totp_at(&secret, claimed, DIGITS);
419        assert_eq!(
420            verify_second_factor(&mut user, &code, db.clone())
421                .await
422                .unwrap(),
423            MfaOutcome::Replayed
424        );
425
426        // The next step's code is accepted, then it too is spent.
427        let next = totp_at(&secret, step_at(now_secs()) + 1, DIGITS);
428        assert_eq!(
429            verify_second_factor(&mut user, &next, db.clone())
430                .await
431                .unwrap(),
432            MfaOutcome::Accepted {
433                via: MfaMethod::Totp,
434                recovery_codes_left: recovery::CODE_COUNT as i64,
435            }
436        );
437        assert_eq!(
438            verify_second_factor(&mut user, &next, db).await.unwrap(),
439            MfaOutcome::Replayed
440        );
441    }
442
443    #[tokio::test]
444    async fn a_wrong_code_is_rejected_without_touching_the_replay_guard() {
445        let db = db().await;
446        let (mut user, secret) = enrolled(db.clone()).await;
447        let claimed = user.totp_last_step;
448
449        for wrong in ["000000", "12345", "abcdef", ""] {
450            assert_eq!(
451                verify_second_factor(&mut user, wrong, db.clone())
452                    .await
453                    .unwrap(),
454                MfaOutcome::Rejected,
455                "submission {wrong:?}"
456            );
457        }
458        assert_eq!(
459            user.totp_last_step, claimed,
460            "a wrong code must not advance the guard, or it would lock out the right one"
461        );
462
463        // And the guard really is where it was: the next step still works.
464        let next = totp_at(&secret, step_at(now_secs()) + 1, DIGITS);
465        assert!(matches!(
466            verify_second_factor(&mut user, &next, db).await.unwrap(),
467            MfaOutcome::Accepted { .. }
468        ));
469    }
470
471    #[tokio::test]
472    async fn a_recovery_code_is_accepted_once_and_decrements_the_count() {
473        let db = db().await;
474        let mut user = operator(db.clone()).await;
475        let enrolment = begin_totp_enrolment(&mut user, "http://localhost:3001", db.clone())
476            .await
477            .unwrap();
478        let code = totp_at(&enrolment.secret, step_at(now_secs()), DIGITS);
479        let codes = confirm_totp_enrolment(&mut user, &code, None, db.clone())
480            .await
481            .unwrap()
482            .unwrap();
483
484        // Submitted exactly as the page rendered it, separator and all.
485        assert_eq!(
486            verify_second_factor(&mut user, &codes[0], db.clone())
487                .await
488                .unwrap(),
489            MfaOutcome::Accepted {
490                via: MfaMethod::RecoveryCode,
491                recovery_codes_left: recovery::CODE_COUNT as i64 - 1,
492            }
493        );
494        assert_eq!(
495            verify_second_factor(&mut user, &codes[0], db.clone())
496                .await
497                .unwrap(),
498            MfaOutcome::Rejected,
499            "single-use: a spent code is worth nothing"
500        );
501
502        // And retyped the ways a human retypes one.
503        assert_eq!(
504            verify_second_factor(&mut user, &codes[1].to_lowercase(), db.clone())
505                .await
506                .unwrap(),
507            MfaOutcome::Accepted {
508                via: MfaMethod::RecoveryCode,
509                recovery_codes_left: recovery::CODE_COUNT as i64 - 2,
510            }
511        );
512        assert_eq!(
513            verify_second_factor(&mut user, &codes[2].replace('-', " "), db.clone())
514                .await
515                .unwrap(),
516            MfaOutcome::Accepted {
517                via: MfaMethod::RecoveryCode,
518                recovery_codes_left: recovery::CODE_COUNT as i64 - 3,
519            }
520        );
521
522        assert_eq!(
523            recovery_codes_remaining(&user.id, db).await.unwrap(),
524            recovery::CODE_COUNT as i64 - 3
525        );
526    }
527
528    #[tokio::test]
529    async fn regenerating_supersedes_the_previous_set() {
530        let db = db().await;
531        let (mut user, _) = enrolled(db.clone()).await;
532
533        let first = AdminRecoveryCode::list_unused(&user.id, &db).await.unwrap();
534        let second = regenerate_recovery_codes(&user, db.clone()).await.unwrap();
535        assert_eq!(second.len(), recovery::CODE_COUNT);
536
537        // A code from the old set is worth nothing now. Verify through the
538        // stored hashes rather than the plaintext, which the first call never
539        // returned.
540        for code in AdminRecoveryCode::list_unused(&user.id, &db).await.unwrap() {
541            assert!(first.iter().all(|old| old.id != code.id));
542        }
543        assert_eq!(
544            verify_second_factor(&mut user, &second[0], db.clone())
545                .await
546                .unwrap(),
547            MfaOutcome::Accepted {
548                via: MfaMethod::RecoveryCode,
549                recovery_codes_left: recovery::CODE_COUNT as i64 - 1,
550            }
551        );
552    }
553
554    #[tokio::test]
555    async fn disabling_clears_every_column_and_every_code() {
556        let db = db().await;
557        let (mut user, _) = enrolled(db.clone()).await;
558        assert!(user.has_totp());
559
560        disable_totp(&mut user, None, db.clone()).await.unwrap();
561
562        assert!(!user.has_totp());
563        assert!(!user.has_pending_totp());
564        assert_eq!(user.totp_last_step, None);
565        assert_eq!(
566            recovery_codes_remaining(&user.id, db.clone())
567                .await
568                .unwrap(),
569            0,
570            "a recovery code for a factor that no longer exists is a second password"
571        );
572
573        // Re-read from the database: the in-memory sync must not be the only
574        // place this happened.
575        let reloaded = AdminUser::find_by_id(&user.id, &db).await.unwrap().unwrap();
576        assert!(!reloaded.has_totp());
577        assert!(!reloaded.has_pending_totp());
578        assert_eq!(reloaded.totp_last_step, None);
579    }
580
581    #[tokio::test]
582    async fn a_factor_change_revokes_every_other_session() {
583        let db = db().await;
584        let mut user = operator(db.clone()).await;
585
586        let kept = AdminSession::create(
587            NewSession {
588                user_id: &user.id,
589                token_hash: "kept-hash",
590                csrf_token: "csrf",
591                created_ip: None,
592                user_agent: None,
593            },
594            std::time::Duration::from_secs(3600),
595            &db,
596        )
597        .await
598        .unwrap();
599        AdminSession::create(
600            NewSession {
601                user_id: &user.id,
602                token_hash: "other-hash",
603                csrf_token: "csrf",
604                created_ip: None,
605                user_agent: None,
606            },
607            std::time::Duration::from_secs(3600),
608            &db,
609        )
610        .await
611        .unwrap();
612
613        let enrolment = begin_totp_enrolment(&mut user, "http://localhost:3001", db.clone())
614            .await
615            .unwrap();
616        let code = totp_at(&enrolment.secret, step_at(now_secs()), DIGITS);
617        confirm_totp_enrolment(&mut user, &code, Some(&kept.token_hash), db.clone())
618            .await
619            .unwrap()
620            .unwrap();
621
622        let live = AdminSession::list_all(Some(&user.id), &db).await.unwrap();
623        assert_eq!(live.len(), 1, "every other browser must be signed out");
624        assert_eq!(live[0].token_hash, "kept-hash");
625
626        // And disabling takes the last one too, when nothing is kept.
627        disable_totp(&mut user, None, db.clone()).await.unwrap();
628        assert!(
629            AdminSession::list_all(Some(&user.id), &db)
630                .await
631                .unwrap()
632                .is_empty()
633        );
634    }
635
636    #[tokio::test]
637    async fn every_outcome_names_itself_for_the_log() {
638        assert_eq!(
639            MfaOutcome::Accepted {
640                via: MfaMethod::Totp,
641                recovery_codes_left: 10
642            }
643            .reason(),
644            ""
645        );
646        assert_eq!(MfaOutcome::Rejected.reason(), "wrong_code");
647        assert_eq!(MfaOutcome::Replayed.reason(), "replayed");
648        assert_eq!(MfaOutcome::NotEnrolled.reason(), "no_factor");
649        assert_eq!(MfaMethod::Totp.as_str(), "totp");
650        assert_eq!(MfaMethod::RecoveryCode.as_str(), "recovery_code");
651    }
652
653    #[tokio::test]
654    async fn the_startup_count_sees_only_confirmed_factors() {
655        let db = db().await;
656        let mut alice = operator(db.clone()).await;
657        AdminUser::create("bob", "hash", &db).await.unwrap();
658        assert_eq!(operators_without_a_factor(db.clone()).await.unwrap(), 2);
659
660        // A pending enrolment is not a factor, so it does not clear the count.
661        let enrolment = begin_totp_enrolment(&mut alice, "http://localhost:3001", db.clone())
662            .await
663            .unwrap();
664        assert_eq!(operators_without_a_factor(db.clone()).await.unwrap(), 2);
665
666        let code = totp_at(&enrolment.secret, step_at(now_secs()), DIGITS);
667        confirm_totp_enrolment(&mut alice, &code, None, db.clone())
668            .await
669            .unwrap()
670            .unwrap();
671        assert_eq!(operators_without_a_factor(db).await.unwrap(), 1);
672    }
673}