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}