Skip to main content

magic_wormhole/
core.rs

1pub(super) mod key;
2pub mod rendezvous;
3mod server_messages;
4#[cfg(test)]
5mod test;
6
7/// Module for wormhole code generation and completion.
8pub mod wordlist;
9
10use serde_derive::{Deserialize, Serialize};
11use std::{borrow::Cow, str::FromStr};
12use thiserror::Error;
13
14use crate::Wordlist;
15
16use self::{rendezvous::*, server_messages::EncryptedMessage};
17
18use crypto_secretbox as secretbox;
19
20/// An error occurred in the wormhole connection
21#[derive(Debug, thiserror::Error)]
22#[non_exhaustive]
23pub enum WormholeError {
24    /// Corrupt message received from peer. Some deserialization went wrong, we probably got some garbage
25    #[error("Corrupt message received from peer")]
26    ProtocolJson(#[from] serde_json::Error),
27    /// Error with the rendezvous server connection. Some deserialization went wrong, we probably got some garbage
28    #[error("Error with the rendezvous server connection")]
29    ServerError(#[from] rendezvous::RendezvousError),
30    /// A generic string message for "something went wrong", i.e.
31    /// the server sent some bullshit message order
32    #[error("Protocol error: {}", _0)]
33    Protocol(Box<str>),
34    /// Key confirmation failed. If you didn't mistype the code,
35    /// this is a sign of an attacker guessing passwords. Please try
36    /// again some time later.
37    #[error(
38        "Key confirmation failed. If you didn't mistype the code, \
39        this is a sign of an attacker guessing passwords. Please try \
40        again some time later."
41    )]
42    PakeFailed,
43    /// Cannot decrypt a received message
44    #[error("Cannot decrypt a received message")]
45    Crypto,
46    /// Nameplate is unclaimed
47    #[error("Nameplate is unclaimed: {}", _0)]
48    UnclaimedNameplate(Nameplate),
49    /// The provided code is invalid
50    #[error("The provided code is invalid: {_0}")]
51    CodeInvalid(#[from] ParseCodeError),
52}
53
54impl WormholeError {
55    /** Should we tell the server that we are "errory" or "scared"? */
56    pub fn is_scared(&self) -> bool {
57        matches!(self, Self::PakeFailed)
58    }
59}
60
61impl From<std::convert::Infallible> for WormholeError {
62    fn from(_: std::convert::Infallible) -> Self {
63        unreachable!()
64    }
65}
66
67/**
68 * Establishing Wormhole connection
69 *
70 * You can send and receive arbitrary messages in form of byte slices over it, using [`Wormhole::send`] and [`Wormhole::receive`].
71 * Everything else (including encryption) will be handled for you.
72 *
73 * To create a wormhole, use the mailbox connection created via [`MailboxConnection::create`] or [`MailboxConnection::connect`] with the [`Wormhole::connect`] method.
74 * Typically, the sender side connects without a code (which will create one), and the receiver side has one (the user entered it, who got it from the sender).
75 *
76 * # Clean shutdown
77 *
78 * TODO
79 */
80/* TODO
81 * Maybe a better way to handle application level protocols is to create a trait for them and then
82 * to paramterize over them.
83 */
84/// A `MailboxConnection` contains a `RendezvousServer` which is connected to the mailbox
85pub struct MailboxConnection<V: serde::Serialize + Send + Sync + 'static> {
86    /// A copy of `AppConfig`,
87    config: AppConfig<V>,
88    /// The `RendezvousServer` with an open mailbox connection
89    server: RendezvousServer,
90    /// The welcome message received from the mailbox server
91    welcome: Option<String>,
92    /// The mailbox id of the created mailbox
93    mailbox: Mailbox,
94    /// The Code which is required to connect to the mailbox.
95    code: Code,
96}
97
98impl<V: serde::Serialize + Send + Sync + 'static> MailboxConnection<V> {
99    /// Create a connection to a mailbox which is configured with a `Code` starting with the nameplate and by a given number of wordlist based random words.
100    ///
101    /// # Arguments
102    ///
103    /// * `config`: Application configuration
104    /// * `code_length`: number of words used for the password. The words are taken from the default wordlist.
105    ///
106    /// # Examples
107    ///
108    /// ```no_run
109    /// # fn main() -> eyre::Result<()> { async_io::block_on(async {
110    /// use magic_wormhole::{AppConfig, MailboxConnection, transfer::APP_CONFIG};
111    /// let config = APP_CONFIG;
112    /// let mailbox_connection = MailboxConnection::create(config, 2).await?;
113    /// # Ok(()) })}
114    /// ```
115    pub async fn create(config: AppConfig<V>, code_length: usize) -> Result<Self, WormholeError> {
116        Self::create_with_validated_password(
117            config,
118            Wordlist::default_wordlist(code_length).choose_words(),
119        )
120        .await
121    }
122
123    /// Create a connection to a mailbox which is configured with a `Code` containing the nameplate and the given password.
124    ///
125    /// # Arguments
126    ///
127    /// * `config`: Application configuration
128    /// * `password`: Free text password which will be appended to the nameplate number to form the `Code`
129    ///
130    /// # Examples
131    ///
132    /// ```no_run
133    /// # #[cfg(feature = "entropy")]
134    /// # {
135    /// # fn main() -> eyre::Result<()> { async_io::block_on(async {
136    /// use magic_wormhole::{MailboxConnection, transfer::APP_CONFIG};
137    /// let config = APP_CONFIG;
138    /// let mailbox_connection =
139    ///     MailboxConnection::create_with_password(config, "secret".parse()?).await?;
140    /// # Ok(()) })}
141    /// # }
142    /// ```
143    ///
144    /// TODO: Replace this with create_with_validated_password
145    pub async fn create_with_password(
146        config: AppConfig<V>,
147        password: Password,
148    ) -> Result<Self, WormholeError> {
149        Self::create_with_validated_password(config, password).await
150    }
151
152    /// Create a connection to a mailbox which is configured with a `Code` containing the nameplate and the given password.
153    ///
154    /// # Arguments
155    ///
156    /// * `config`: Application configuration
157    /// * `password`: Free text password which will be appended to the nameplate number to form the `Code`
158    async fn create_with_validated_password(
159        config: AppConfig<V>,
160        password: Password,
161    ) -> Result<Self, WormholeError> {
162        let (mut server, welcome) =
163            RendezvousServer::connect(&config.id, &config.rendezvous_url).await?;
164        let (nameplate, mailbox) = server.allocate_claim_open().await?;
165        let code = Code::from_components(nameplate, password);
166
167        Ok(MailboxConnection {
168            config,
169            server,
170            mailbox,
171            code,
172            welcome,
173        })
174    }
175
176    /// Create a connection to a mailbox defined by a `Code` which contains the `Nameplate` and the password to authorize the access.
177    ///
178    /// # Arguments
179    ///
180    /// * `config`: Application configuration
181    /// * `code`: The `Code` required to authorize to connect to an existing mailbox.
182    /// * `allocate`:
183    ///   - `true`: Allocates a `Nameplate` if it does not exist.
184    ///   - `false`: The call fails with a `WormholeError::UnclaimedNameplate` when the `Nameplate` does not exist.
185    ///
186    /// # Examples
187    ///
188    /// ```no_run
189    /// # fn main() -> eyre::Result<()> { async_io::block_on(async {
190    /// use magic_wormhole::{Code, MailboxConnection, Nameplate, transfer::APP_CONFIG};
191    /// let config = APP_CONFIG;
192    /// let code = "5-password".parse()?;
193    /// let mailbox_connection = MailboxConnection::connect(config, code, false).await?;
194    /// # Ok(()) })}
195    /// ```
196    pub async fn connect(
197        config: AppConfig<V>,
198        code: Code,
199        allocate: bool,
200    ) -> Result<Self, WormholeError> {
201        let (mut server, welcome) =
202            RendezvousServer::connect(&config.id, &config.rendezvous_url).await?;
203        let nameplate = code.nameplate();
204
205        // Ensure the code has enough entropy without the nameplate [#193](https://github.com/magic-wormhole/magic-wormhole.rs/issues/193)
206
207        if !allocate {
208            let nameplates = server.list_nameplates().await?;
209            if !nameplates.contains(&nameplate) {
210                server.shutdown(Mood::Errory).await?;
211                return Err(WormholeError::UnclaimedNameplate(nameplate));
212            }
213        }
214        let mailbox = server.claim_open(nameplate).await?;
215
216        Ok(MailboxConnection {
217            config,
218            server,
219            mailbox,
220            code,
221            welcome,
222        })
223    }
224
225    /// Shut down the connection to the mailbox
226    ///
227    /// # Arguments
228    ///
229    /// * `mood`: `Mood` should give a hint of the reason of the shutdown
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// # fn main() -> eyre::Result<()> { use magic_wormhole::WormholeError;
235    /// # #[cfg(feature = "entropy")]
236    /// return async_io::block_on(async {
237    /// use magic_wormhole::{transfer::APP_CONFIG, MailboxConnection, Mood};
238    /// let config = APP_CONFIG;
239    /// let mailbox_connection = MailboxConnection::create_with_password(config, "secret-code-password".parse()?)
240    ///     .await?;
241    /// mailbox_connection.shutdown(Mood::Happy).await?;
242    /// # Ok(())});
243    /// # #[cfg(not(feature = "entropy"))]
244    /// # return Ok(());
245    /// # }
246    /// ```
247    pub async fn shutdown(self, mood: Mood) -> Result<(), WormholeError> {
248        self.server
249            .shutdown(mood)
250            .await
251            .map_err(WormholeError::ServerError)
252    }
253
254    /// The welcome message received from the mailbox server
255    pub fn welcome(&self) -> Option<&str> {
256        self.welcome.as_deref()
257    }
258
259    /// The Code that was used to connect to the mailbox.
260    pub fn code(&self) -> &Code {
261        &self.code
262    }
263}
264
265/// A wormhole is an open connection to a peer via the rendezvous server.
266///
267/// This establishes the client-client part of the connection setup.
268#[derive(Debug)]
269pub struct Wormhole {
270    server: RendezvousServer,
271    phase: u64,
272    key: key::Key<key::WormholeKey>,
273    appid: AppID,
274    /// The cryptographic verifier code for the connection
275    verifier: Box<secretbox::Key>,
276    /// Our app version
277    our_version: Box<dyn std::any::Any + Send + Sync>,
278    /// The app version of the peer
279    peer_version: serde_json::Value,
280}
281
282impl Wormhole {
283    /// Set up a Wormhole which is the client-client part of the connection setup
284    ///
285    /// The MailboxConnection already contains a rendezvous server with an opened mailbox.
286    pub async fn connect(
287        mailbox_connection: MailboxConnection<impl serde::Serialize + Send + Sync + 'static>,
288    ) -> Result<Self, WormholeError> {
289        let MailboxConnection {
290            config,
291            mut server,
292            mailbox: _mailbox,
293            code,
294            welcome: _welcome,
295        } = mailbox_connection;
296
297        /* Send PAKE */
298        let (pake_state, pake_msg_ser) = key::make_pake(code.as_str(), &config.id);
299        server.send_peer_message(Phase::PAKE, pake_msg_ser).await?;
300
301        /* Receive PAKE */
302        let peer_pake = key::extract_pake_msg(&server.next_peer_message_some().await?.body)?;
303        let key = pake_state
304            .finish(&peer_pake)
305            .map_err(|_| WormholeError::PakeFailed)
306            .map(|key| *secretbox::Key::from_slice(&key))?;
307
308        /* Send versions message */
309        let mut versions = key::VersionsMessage::new();
310        versions.set_app_versions(serde_json::to_value(&config.app_version).unwrap());
311        let (version_phase, version_msg) = key::build_version_msg(server.side(), &key, &versions);
312        server.send_peer_message(version_phase, version_msg).await?;
313        let peer_version = server.next_peer_message_some().await?;
314
315        /* Handle received message */
316        let versions: key::VersionsMessage = peer_version
317            .decrypt(&key)
318            .ok_or(WormholeError::PakeFailed)
319            .and_then(|plaintext| {
320                serde_json::from_slice(&plaintext).map_err(WormholeError::ProtocolJson)
321            })?;
322
323        let peer_version = versions.app_versions;
324
325        if server.needs_nameplate_release() {
326            server.release_nameplate().await?;
327        }
328
329        tracing::info!("Found peer on the rendezvous server.");
330
331        /* We are now fully initialized! Up and running! :tada: */
332        Ok(Self {
333            server,
334            appid: config.id,
335            phase: 0,
336            key: key::Key::new(key.into()),
337            verifier: Box::new(key::derive_verifier(&key)),
338            our_version: Box::new(config.app_version),
339            peer_version,
340        })
341    }
342
343    /** Send an encrypted message to peer */
344    pub async fn send(&mut self, plaintext: Vec<u8>) -> Result<(), WormholeError> {
345        let phase_string = Phase::numeric(self.phase);
346        self.phase += 1;
347        let data_key = key::derive_phase_key(self.server.side(), self.key.as_ref(), &phase_string);
348        let (_nonce, encrypted) = key::encrypt_data(&data_key, &plaintext);
349        self.server
350            .send_peer_message(phase_string, encrypted)
351            .await?;
352        Ok(())
353    }
354
355    /**
356     * Serialize and send an encrypted message to peer
357     *
358     * This will serialize the message as `json` string, which is most commonly
359     * used by upper layer protocols. The serialization may not fail
360     *
361     * ## Panics
362     *
363     * If the serialization fails
364     */
365    pub async fn send_json<T: serde::Serialize>(
366        &mut self,
367        message: &T,
368    ) -> Result<(), WormholeError> {
369        self.send(serde_json::to_vec(message).unwrap()).await
370    }
371
372    /** Receive an encrypted message from peer */
373    pub async fn receive(&mut self) -> Result<Vec<u8>, WormholeError> {
374        loop {
375            let peer_message = match self.server.next_peer_message().await? {
376                Some(peer_message) => peer_message,
377                None => continue,
378            };
379            if peer_message.phase.to_num().is_none() {
380                // TODO: log and ignore, for future expansion
381                todo!("log and ignore, for future expansion");
382            }
383
384            // TODO maybe reorder incoming messages by phase numeral?
385            let decrypted_message = peer_message
386                .decrypt(self.key.as_ref())
387                .ok_or(WormholeError::Crypto)?;
388
389            // Send to client
390            return Ok(decrypted_message);
391        }
392    }
393
394    /**
395     * Receive an encrypted message from peer
396     *
397     * This will deserialize the message as `json` string, which is most commonly
398     * used by upper layer protocols. We distinguish between the different layers
399     * on which a serialization error happened, hence the double `Result`.
400     */
401    pub async fn receive_json<T>(&mut self) -> Result<Result<T, serde_json::Error>, WormholeError>
402    where
403        T: for<'a> serde::Deserialize<'a>,
404    {
405        self.receive().await.map(|data: Vec<u8>| {
406            serde_json::from_slice(&data).inspect_err(|_| {
407                tracing::error!(
408                    "Received invalid data from peer: '{}'",
409                    String::from_utf8_lossy(&data)
410                );
411            })
412        })
413    }
414
415    /// Close the wormhole
416    pub async fn close(self) -> Result<(), WormholeError> {
417        tracing::debug!("Closing Wormhole…");
418        self.server.shutdown(Mood::Happy).await.map_err(Into::into)
419    }
420
421    /**
422     * The `AppID` this wormhole is bound to.
423     * This determines the upper-layer protocol. Only wormholes with the same value can talk to each other.
424     */
425    pub fn appid(&self) -> &AppID {
426        &self.appid
427    }
428
429    /**
430     * The symmetric encryption key used by this connection.
431     * Can be used to derive sub-keys for different purposes.
432     */
433    pub fn key(&self) -> &key::Key<key::WormholeKey> {
434        &self.key
435    }
436
437    /**
438     * If you're paranoid, let both sides check that they calculated the same verifier.
439     *
440     * PAKE hardens a standard key exchange with a password ("password authenticated") in order
441     * to mitigate potential man in the middle attacks that would otherwise be possible. Since
442     * the passwords usually are not of hight entropy, there is a low-probability possible of
443     * an attacker guessing the password correctly, enabling them to MitM the connection.
444     *
445     * Not only is that probability low, but they also have only one try per connection and a failed
446     * attempts will be noticed by both sides. Nevertheless, comparing the verifier mitigates that
447     * attack vector.
448     */
449    pub fn verifier(&self) -> &secretbox::Key {
450        &self.verifier
451    }
452
453    /**
454     * Our "app version" information that we sent. See the [`peer_version`](Self::peer_version()) for more information.
455     */
456    pub fn our_version(&self) -> &(dyn std::any::Any + Send + Sync) {
457        &*self.our_version
458    }
459
460    /**
461     * Protocol version information from the other side.
462     * This is bound by the [`AppID`]'s protocol and thus shall be handled on a higher level
463     * (e.g. by the file transfer API).
464     */
465    pub fn peer_version(&self) -> &serde_json::Value {
466        &self.peer_version
467    }
468}
469
470/// The close command accepts an optional "mood" string: this allows clients to tell the server
471/// (in general terms) about their experiences with the wormhole interaction. The server records
472/// the mood in its "usage" record, so the server operator can get a sense of how many connections
473/// are succeeding and failing. The moods currently recognized by the Mailbox server are:
474#[derive(Debug, PartialEq, Copy, Clone, Deserialize, Serialize, derive_more::Display)]
475pub enum Mood {
476    /// The PAKE key-establishment worked, and the client saw at least one valid encrypted message from its peer
477    #[serde(rename = "happy")]
478    Happy,
479    /// The client gave up without hearing anything from its peer
480    #[serde(rename = "lonely")]
481    Lonely,
482    /// The client encountered some other error: protocol problem or internal error
483    #[serde(rename = "errory")]
484    Errory,
485    /// The client saw an invalid encrypted message from its peer,
486    /// indicating that either the wormhole code was typed in wrong,
487    /// or an attacker tried (and failed) to guess the code
488    #[serde(rename = "scary")]
489    Scared,
490    /// Clients are not welcome on the server right now
491    #[serde(rename = "unwelcome")]
492    Unwelcome,
493}
494
495/**
496 * Wormhole configuration corresponding to an uppler layer protocol
497 *
498 * There are multiple different protocols built on top of the core
499 * Wormhole protocol. They are identified by a unique URI-like ID string
500 * (`AppID`), an URL to find the rendezvous server (might be shared among
501 * multiple protocols), and client implementations also have a "version"
502 * data to do protocol negotiation.
503 *
504 * See [`crate::transfer::APP_CONFIG`].
505 */
506#[derive(PartialEq, Eq, Clone, Debug)]
507pub struct AppConfig<V> {
508    /// The ID of the used application
509    pub id: AppID,
510    /// The URL of the rendezvous server
511    pub rendezvous_url: Cow<'static, str>,
512    /// The client application version
513    pub app_version: V,
514}
515
516impl<V> AppConfig<V> {
517    /// Set the app id
518    pub fn id(mut self, id: AppID) -> Self {
519        self.id = id;
520        self
521    }
522
523    /// Set the rendezvous URL
524    pub fn rendezvous_url(mut self, rendezvous_url: Cow<'static, str>) -> Self {
525        self.rendezvous_url = rendezvous_url;
526        self
527    }
528}
529
530impl<V: serde::Serialize> AppConfig<V> {
531    /// Set the app version
532    pub fn app_version(mut self, app_version: V) -> Self {
533        self.app_version = app_version;
534        self
535    }
536}
537
538/// Newtype wrapper for application IDs
539///
540/// The application ID is a string that scopes all commands
541/// to that name, effectively separating different protocols
542/// on the same rendezvous server.
543#[derive(
544    PartialEq, Eq, Clone, Debug, Deserialize, Serialize, derive_more::Display, derive_more::Deref,
545)]
546#[deref(forward)]
547pub struct AppID(#[deref] pub(crate) Cow<'static, str>);
548
549impl AppID {
550    /// Create a new app ID from an ID string
551    pub fn new(id: impl Into<Cow<'static, str>>) -> Self {
552        AppID(id.into())
553    }
554}
555
556impl From<String> for AppID {
557    fn from(s: String) -> Self {
558        Self::new(s)
559    }
560}
561
562impl AsRef<str> for AppID {
563    fn as_ref(&self) -> &str {
564        &self.0
565    }
566}
567
568// MySide is used for the String that we send in all our outbound messages
569#[derive(
570    PartialEq, Eq, Clone, Debug, Deserialize, Serialize, derive_more::Display, derive_more::Deref,
571)]
572#[serde(transparent)]
573#[display("MySide({})", "&*_0")]
574pub(crate) struct MySide(EitherSide);
575
576impl MySide {
577    pub fn generate() -> MySide {
578        use rand::{RngCore, rngs::OsRng};
579
580        let mut bytes: [u8; 5] = [0; 5];
581        OsRng.fill_bytes(&mut bytes);
582
583        MySide(EitherSide(hex::encode(bytes)))
584    }
585
586    // It's a minor type system feature that converting an arbitrary string into MySide is hard.
587    // This prevents it from getting swapped around with TheirSide.
588    #[cfg(test)]
589    pub fn unchecked_from_string(s: String) -> MySide {
590        MySide(EitherSide(s))
591    }
592}
593
594// TheirSide is used for the string that arrives inside inbound messages
595#[derive(
596    PartialEq, Eq, Clone, Debug, Deserialize, Serialize, derive_more::Display, derive_more::Deref,
597)]
598#[serde(transparent)]
599#[display("TheirSide({})", "&*_0")]
600pub(crate) struct TheirSide(EitherSide);
601
602impl<S: Into<String>> From<S> for TheirSide {
603    fn from(s: S) -> TheirSide {
604        TheirSide(EitherSide(s.into()))
605    }
606}
607
608#[derive(
609    PartialEq, Eq, Clone, Debug, Deserialize, Serialize, derive_more::Display, derive_more::Deref,
610)]
611#[serde(transparent)]
612#[deref(forward)]
613#[display("{}", "&*_0")]
614pub(crate) struct EitherSide(pub String);
615
616impl<S: Into<String>> From<S> for EitherSide {
617    fn from(s: S) -> EitherSide {
618        EitherSide(s.into())
619    }
620}
621
622#[derive(PartialEq, Eq, Clone, Debug, Hash, Deserialize, Serialize, derive_more::Display)]
623#[serde(transparent)]
624pub(crate) struct Phase(Cow<'static, str>);
625
626impl Phase {
627    pub const VERSION: Self = Phase(Cow::Borrowed("version"));
628    pub const PAKE: Self = Phase(Cow::Borrowed("pake"));
629
630    pub fn numeric(phase: u64) -> Self {
631        Phase(phase.to_string().into())
632    }
633
634    #[allow(dead_code)]
635    pub fn is_version(&self) -> bool {
636        self == &Self::VERSION
637    }
638
639    #[allow(dead_code)]
640    pub fn is_pake(&self) -> bool {
641        self == &Self::PAKE
642    }
643
644    pub fn to_num(&self) -> Option<u64> {
645        self.0.parse().ok()
646    }
647}
648
649impl AsRef<str> for Phase {
650    fn as_ref(&self) -> &str {
651        &self.0
652    }
653}
654
655#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize, derive_more::Display)]
656#[serde(transparent)]
657pub(crate) struct Mailbox(pub String);
658
659/// An error occurred when parsing a nameplate: Nameplate is not a number.
660#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Copy, derive_more::Display, Error)]
661#[display("Nameplate is not a number. Nameplates must be a number >= 1.")]
662#[non_exhaustive]
663pub struct ParseNameplateError {}
664
665/// Wormhole codes look like 4-purple-sausages, consisting of a number followed by some random words.
666/// This number is called a "Nameplate".
667#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize, derive_more::Display)]
668#[serde(transparent)]
669#[display("{}", _0)]
670pub struct Nameplate(String);
671
672impl Nameplate {
673    /// Create a new nameplate from a string.
674    ///
675    /// Safety: Nameplate will be [required to be numbers](https://github.com/magic-wormhole/magic-wormhole-mailbox-server/issues/45) soon.
676    #[expect(unsafe_code)]
677    #[doc(hidden)]
678    pub unsafe fn new_unchecked(n: &str) -> Self {
679        Nameplate(n.into())
680    }
681}
682
683impl FromStr for Nameplate {
684    type Err = ParseNameplateError;
685
686    fn from_str(s: &str) -> Result<Self, Self::Err> {
687        if !s.chars().all(|c| c.is_ascii_digit()) || u128::from_str(s) == Ok(0) {
688            Err(ParseNameplateError {})
689        } else {
690            Ok(Self(s.to_string()))
691        }
692    }
693}
694
695/// This is a compromise. Generally we want to be wordlist-agnostic here. But
696/// we can't ignore that the PGP wordlist is the most common wordlist in use.
697///
698/// - The shortest word in the PGP wordlist is 4 characters long. The longest
699///   word is 9 characters. This means we can't limit to more than 9 bytes here,
700///   'solo-orca' is 9 bytes, and we want to allow 2-word codes.
701/// - A 9 character random password is very strong. If it is only comprised of
702///   uniformly distributed lowercase ASCII characters, we have an entropy of
703///   26^9 >= 40 bits. This is much more than the default 16 bits we get from two
704///   words from the PGP wordlist.
705/// - An emoji contains at least 2 bytes of data. So two emoji are actually
706///   about the same security as two words from the PGP wordlist.
707///
708/// We ultimately can't protect people from making bad choices, as entropy is a
709/// very difficult thing. What we can do instead is offer guidance, by printing
710/// warnings in case of rather short passwords, and making people choose for
711/// themselves whether their privacy is worth it to them choosing longer codes.
712#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Copy, derive_more::Display, Error)]
713#[non_exhaustive]
714pub enum ParsePasswordError {
715    /// Password too short
716    #[display("Password too short. It is only {value} bytes, but must be at least {required}")]
717    TooShort {
718        /// The calculated value
719        value: usize,
720        /// The value that is required
721        required: usize,
722    },
723    /// Password does not have enough entropy
724    #[display(
725        "Password too weak. It can be guessed with an average of {value} tries, but must be at least {required}"
726    )]
727    LittleEntropy {
728        /// The calculated value
729        value: u64,
730        /// The value that is required
731        required: u64,
732    },
733}
734
735/// Wormhole codes look like 4-purple-sausages, consisting of a number followed by some random words.
736/// This number is called a "Nameplate", the rest is called the `Password`
737#[derive(Clone, Debug, Serialize, derive_more::Display)]
738#[serde(transparent)]
739#[display("{password}")]
740pub struct Password {
741    password: String,
742    #[serde(skip)]
743    entropy: zxcvbn::Entropy,
744}
745
746impl PartialEq for Password {
747    fn eq(&self, other: &Self) -> bool {
748        self.password == other.password
749    }
750}
751
752impl Eq for Password {}
753
754impl Password {
755    /// Create a new password from a string. Does not check the entropy of the password.
756    ///
757    /// You should use [`Password::from_str`] / [`String::parse`] instead.
758    ///
759    /// Safety: Does not check the entropy of the password, or if one exists at all. This can be a security risk.
760    #[expect(unsafe_code)]
761    #[doc(hidden)]
762    pub unsafe fn new_unchecked(n: impl Into<String>) -> Self {
763        let password = n.into();
764        let entropy = Self::calculate_entropy(&password);
765
766        Password { password, entropy }
767    }
768
769    fn calculate_entropy(password: &str) -> zxcvbn::Entropy {
770        static PGP_WORDLIST: std::sync::OnceLock<Vec<&str>> = std::sync::OnceLock::new();
771        let words = PGP_WORDLIST.get_or_init(|| {
772            // TODO: We leak the str: https://github.com/shssoichiro/zxcvbn-rs/issues/87
773            Wordlist::default_wordlist(2)
774                .into_words()
775                .map(|s| &*s.leak())
776                .collect::<Vec<_>>()
777        });
778
779        zxcvbn::zxcvbn(password, &words[..])
780    }
781}
782
783impl From<Password> for String {
784    fn from(value: Password) -> Self {
785        value.password
786    }
787}
788
789impl AsRef<str> for Password {
790    fn as_ref(&self) -> &str {
791        &self.password
792    }
793}
794
795impl FromStr for Password {
796    type Err = ParsePasswordError;
797
798    fn from_str(password: &str) -> Result<Self, Self::Err> {
799        let password = password.to_string();
800
801        if password.len() < 4 {
802            Err(ParsePasswordError::TooShort {
803                value: password.len(),
804                required: 4,
805            })
806        } else {
807            let entropy = Self::calculate_entropy(&password);
808            if entropy.guesses() < 2_u64.pow(16) {
809                return Err(ParsePasswordError::LittleEntropy {
810                    value: entropy.guesses(),
811                    required: 2_u64.pow(16),
812                });
813            }
814            Ok(Self { password, entropy })
815        }
816    }
817}
818
819/// An error occurred parsing the string as a valid wormhole mailbox code
820#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Copy, derive_more::Display, Error)]
821#[non_exhaustive]
822pub enum ParseCodeError {
823    /// The code is empty
824    #[display("The code is empty")]
825    Empty,
826    /// A code must contain at least one '-' to separate nameplate from password
827    #[display("A code must contain at least one '-' to separate nameplate from password")]
828    SeparatorMissing,
829    /// An error occurred when parsing the nameplate
830    #[display("{_0}")]
831    Nameplate(#[from] ParseNameplateError),
832    /// An error occurred when parsing the code
833    #[display("{_0}")]
834    Password(#[from] ParsePasswordError),
835}
836
837/** A wormhole code à la 15-foo-bar
838 *
839 * The part until the first dash is called the "nameplate" and is purely numeric.
840 * The rest is the password and may be arbitrary, although dash-joining words from
841 * a wordlist is a common convention.
842 */
843#[derive(PartialEq, Eq, Clone, Debug, derive_more::Display)]
844#[display("{}", _0)]
845pub struct Code(String);
846
847impl Code {
848    /// Create a new code, comprised of a [`Nameplate`] and a [`Password`].
849    pub fn from_components(nameplate: Nameplate, password: Password) -> Self {
850        Self(format!("{nameplate}-{password}"))
851    }
852
853    /// Retrieve only the nameplate
854    pub fn nameplate(&self) -> Nameplate {
855        // Safety: We checked the validity of the nameplate before
856        #[expect(unsafe_code)]
857        unsafe {
858            Nameplate::new_unchecked(self.0.split('-').next().unwrap())
859        }
860    }
861
862    /// Retrieve only the password
863    pub fn password(&self) -> Password {
864        // Safety: We checked the validity of the password before
865        #[expect(unsafe_code)]
866        unsafe {
867            Password::new_unchecked(self.0.splitn(2, '-').last().unwrap())
868        }
869    }
870
871    pub(crate) fn as_str(&self) -> &str {
872        &self.0
873    }
874}
875
876impl FromStr for Code {
877    type Err = ParseCodeError;
878
879    fn from_str(s: &str) -> Result<Self, Self::Err> {
880        match s.split_once('-') {
881            Some((n, p)) => {
882                let password: Password = p.parse()?;
883                let nameplate: Nameplate = n.parse()?;
884
885                Ok(Self(format!("{nameplate}-{password}")))
886            },
887            None if s.is_empty() => Err(ParseCodeError::Empty),
888            None => Err(ParseCodeError::SeparatorMissing),
889        }
890    }
891}