Repository: duesee/imap-codec (language: python)

Issue:
chore: Remove constant time from `Secret` as it invites to use plain passwords.
In some authentication flows timing seems relevant, i.e., XOAUTH2. But, in the end, we don't provide a complete way to do authentication safely. Even worse, the examples invite to use plaintext credentials. Thus, I'm leaning towards removing all of this. I would still like to keep `Secret` for the sake of guarding against accidental `Debug`-printing of sensitive data, though.


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 2ebec6da..75591349 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,11 +10,10 @@ license = "MIT OR Apache-2.0"
 edition = "2021"
 
 [features]
-default = []
+# <Forward to imap-types>
 arbitrary = ["imap-types/arbitrary"]
 bounded-static = ["dep:bounded-static", "imap-types/bounded-static"]
 serde = ["dep:serde", "chrono/serde", "imap-types/serde"]
-tokio = ["dep:bytes", "dep:tokio-util", "bounded-static"]
 
 # IMAP
 starttls = ["imap-types/starttls"]
@@ -31,18 +30,20 @@ ext_move = ["imap-types/ext_move"]
 ext_quota = ["imap-types/ext_quota"]
 ext_sasl_ir = ["imap-types/ext_sasl_ir"]
 ext_unselect = ["imap-types/ext_unselect"]
+# </Forward to imap-types>
 
 # IMAP quirks
 quirk_crlf_relaxed = []
 
+# Tokio support
+tokio = ["dep:bytes", "dep:tokio-util", "bounded-static"]
+
 [dependencies]
 abnf-core = "0.6.0"
 base64 = "0.21"
 bytes = { version = "1", optional = true }
 bounded-static = { version = "0.5.0", optional = true }
 chrono = { version = "0.4", default-features = false, features = ["alloc"] }
-# The `unchecked` feature unlocks several `unchecked` methods that allow to bypass syntax checks when
-# creating types. This may allow to skip duplicate checks when all preconditions are known to be true.
 imap-types = { path = "imap-types", default-features = false, features = ["unvalidated"] }
 nom = "7"
 serde = { version = "1", features = ["derive"], optional = true }
@@ -80,6 +81,6 @@ members = [
     "fuzz",
     "imap-types",
     "imap-types/fuzz",
-    "assets/demos/tokio_client",
-    "assets/demos/tokio_server",
+    "assets/demos/tokio-client",
+    "assets/demos/tokio-server",
 ]
diff --git a/README.md b/README.md
index 0378bc56..9bd5cf85 100644
--- a/README.md
+++ b/README.md
@@ -56,8 +56,7 @@ $ cargo run --example=parse_command
 You can also start the [demo server] with ...
 
 ```sh
-$ cd assets/demos/tokio_server
-$ cargo run <host>:<port>
+$ cargo run -p tokio-server -- <host>:<port>
 ```
 
 ... and connect to it with ...
@@ -204,6 +203,6 @@ Thanks to the [NLnet Foundation](https://nlnet.nl/) for supporting imap-codec th
 [extensions]: https://docs.rs/imap-codec/latest/imap_codec/#features
 [documentation]: https://docs.rs/imap-codec/latest/imap_codec/
 [cargo fuzz]: https://github.com/rust-fuzz/cargo-fuzz
-[demo client]: https://github.com/duesee/imap-codec/tree/main/assets/demos/tokio_client
-[demo server]: https://github.com/duesee/imap-codec/tree/main/assets/demos/tokio_server
+[demo client]: https://github.com/duesee/imap-codec/tree/main/assets/demos/tokio-client
+[demo server]: https://github.com/duesee/imap-codec/tree/main/assets/demos/tokio-server
 [parse_command]: https://github.com/duesee/imap-codec/blob/main/examples/parse_command.rs
diff --git a/assets/demos/tokio_client/Cargo.toml b/assets/demos/tokio-client/Cargo.toml
similarity index 94%
rename from assets/demos/tokio_client/Cargo.toml
rename to assets/demos/tokio-client/Cargo.toml
index b3d8241a..c2f5645e 100644
--- a/assets/demos/tokio_client/Cargo.toml
+++ b/assets/demos/tokio-client/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "tokio_client"
+name = "tokio-client"
 version = "0.1.0"
 edition = "2021"
 
diff --git a/assets/demos/tokio_client/src/main.rs b/assets/demos/tokio-client/src/main.rs
similarity index 98%
rename from assets/demos/tokio_client/src/main.rs
rename to assets/demos/tokio-client/src/main.rs
index 1782a25a..a72dd4f3 100644
--- a/assets/demos/tokio_client/src/main.rs
+++ b/assets/demos/tokio-client/src/main.rs
@@ -18,7 +18,7 @@ const RESET: &str = "\x1b[0m";
 async fn main() -> Result<(), Error> {
     let addr = std::env::args()
         .nth(1)
-        .context("USAGE: tokio_client <host>:<port>")?;
+        .context("USAGE: tokio-client <host>:<port>")?;
 
     let mut framed = {
         let stream = TcpStream::connect(&addr)
diff --git a/assets/demos/tokio_server/Cargo.toml b/assets/demos/tokio-server/Cargo.toml
similarity index 90%
rename from assets/demos/tokio_server/Cargo.toml
rename to assets/demos/tokio-server/Cargo.toml
index 44cceb32..1d2723a7 100644
--- a/assets/demos/tokio_server/Cargo.toml
+++ b/assets/demos/tokio-server/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "tokio_server"
+name = "tokio-server"
 version = "0.1.0"
 edition = "2021"
 
@@ -7,6 +7,7 @@ edition = "2021"
 
 [dependencies]
 anyhow = "1.0.71"
+argon2 = "0.5.0"
 futures = "0.3"
 imap-codec = { path = "../../../", features = ["tokio"] }
 tokio = { version = "1", features = ["full"] }
diff --git a/assets/demos/tokio_server/src/main.rs b/assets/demos/tokio-server/src/main.rs
similarity index 72%
rename from assets/demos/tokio_server/src/main.rs
rename to assets/demos/tokio-server/src/main.rs
index c8231aca..ba28b5eb 100644
--- a/assets/demos/tokio_server/src/main.rs
+++ b/assets/demos/tokio-server/src/main.rs
@@ -1,8 +1,9 @@
 use anyhow::{Context, Error};
+use argon2::Argon2;
 use futures::{SinkExt, StreamExt};
 use imap_codec::{
     command::CommandBody,
-    core::NonEmptyVec,
+    core::{NonEmptyVec, Text},
     response::{Capability, Continue, Data, Greeting, Response, Status},
     tokio::server::{Action, Event, ImapServerCodec},
 };
@@ -18,7 +19,7 @@ const RESET: &str = "\x1b[0m";
 async fn main() -> Result<(), Error> {
     let addr = std::env::args()
         .nth(1)
-        .context("USAGE: tokio_server <host>:<port>")?;
+        .context("USAGE: tokio-server <host>:<port>")?;
 
     let mut framed = {
         let stream = {
@@ -76,18 +77,43 @@ async fn main() -> Result<(), Error> {
                         println!("S: {BLUE}{rsp:#?}{RESET}");
                     }
                     (tag, CommandBody::Login { username, password }) => {
-                        let rsp =
-                            if username.as_ref() == b"alice" && password.compare_with("password") {
-                                Response::Status(
-                                    Status::ok(Some(tag), None, "LOGIN succeeded")
-                                        .context("Could not create `Status`")?,
-                                )
-                            } else {
-                                Response::Status(
-                                    Status::no(Some(tag), None, "LOGIN failed")
-                                        .context("Could not create `Status`")?,
-                                )
+                        let login_okay = {
+                            let username_okay = username.as_ref() == b"alice";
+                            let password_okay = {
+                                // Salt should be unique per password.
+                                let salt = b"hf63l9nx43gf95ks";
+                                let password = password.declassify().as_ref();
+
+                                let mut output = [0u8; 32];
+                                Argon2::default()
+                                    .hash_password_into(password, salt, &mut output)
+                                    .map_err(|error| Error::msg(error.to_string()))
+                                    .context("Failed to hash password.")?;
+
+                                output
+                                    == [
+                                        227, 130, 151, 49, 100, 203, 239, 68, 119, 207, 247, 237,
+                                        214, 42, 85, 208, 198, 107, 116, 35, 64, 122, 143, 68, 236,
+                                        228, 130, 250, 31, 221, 217, 77,
+                                    ]
                             };
+
+                            username_okay && password_okay
+                        };
+
+                        let rsp = if login_okay {
+                            Response::Status(Status::Ok {
+                                tag: Some(tag),
+                                code: None,
+                                text: Text::unvalidated("LOGIN succeeded"),
+                            })
+                        } else {
+                            Response::Status(Status::Ok {
+                                tag: Some(tag),
+                                code: None,
+                                text: Text::unvalidated("LOGIN failed"),
+                            })
+                        };
                         framed.send(&rsp).await.context("Could not send response")?;
                         println!("S: {BLUE}{rsp:#?}{RESET}");
                     }
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index 88d7fc20..9924dd59 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -8,11 +8,12 @@ edition = "2021"
 [package.metadata]
 cargo-fuzz = true
 
-# Forward features according to ...
-#    https://rust-fuzz.github.io/book/cargo-fuzz/guide.html#cargo-features
 [features]
-debug = []
+# <Forward to imap-codec>
+# IMAP
 starttls = ["imap-codec/starttls"]
+
+# IMAP Extensions
 ext_compress = ["imap-codec/ext_compress"]
 ext_condstore_qresync = ["imap-codec/ext_condstore_qresync"]
 ext_enable = ["imap-codec/ext_enable"]
@@ -24,6 +25,11 @@ ext_move = ["imap-codec/ext_move"]
 ext_quota = ["imap-codec/ext_quota"]
 ext_sasl_ir = ["imap-codec/ext_sasl_ir"]
 ext_unselect = ["imap-codec/ext_unselect"]
+
+# IMAP quirks
+quirk_crlf_relaxed = ["imap-codec/quirk_crlf_relaxed"]
+# </Forward to imap-codec>
+
 # Use (most) IMAP extensions.
 ext = [
     "starttls",
@@ -39,7 +45,8 @@ ext = [
     "ext_sasl_ir",
     "ext_unselect",
 ]
-quirk_crlf_relaxed = ["imap-codec/quirk_crlf_relaxed"]
+# Enable `Debug`-printing during parsing. This is useful to analyze crashes.
+debug = []
 
 [dependencies]
 libfuzzer-sys = "0.4"
diff --git a/imap-types/Cargo.toml b/imap-types/Cargo.toml
index da6ddc9f..5d79d94a 100644
--- a/imap-types/Cargo.toml
+++ b/imap-types/Cargo.toml
@@ -11,10 +11,8 @@ edition = "2021"
 
 [features]
 arbitrary = ["dep:arbitrary", "unvalidated", "chrono/arbitrary", "chrono/std"]
-default = []
 bounded-static = ["dep:bounded-static", "bounded-static/derive"]
 serde = ["dep:serde", "chrono/serde"]
-unvalidated = []
 
 # IMAP
 starttls = []
@@ -32,13 +30,15 @@ ext_quota = []
 ext_sasl_ir = []
 ext_unselect = []
 
+# Unlock `unvalidated` constructors.
+unvalidated = []
+
 [dependencies]
 arbitrary = { version = "1.0.1", optional = true, features = ["derive"] }
 base64 = "0.21"
 bounded-static = { version = "0.5.0", optional = true }
 chrono = { version = "0.4", default-features = false, features = ["alloc"] }
 serde = { version = "1.0.103", features = ["derive"], optional = true }
-subtle = "2"
 thiserror = "1.0.29"
 
 [dev-dependencies]
diff --git a/imap-types/README.md b/imap-types/README.md
index 458ceed5..8cf15f46 100644
--- a/imap-types/README.md
+++ b/imap-types/README.md
@@ -95,7 +95,7 @@ alice password
 
 ... even though "alice" could be encoded more simply with an atom or quoted string.
 
-Also, you can use Rust literals and resort to `unchecked` constructors when you are certain that your input is correct:
+Also, you can use Rust literals and resort to `unvalidated` constructors when you are certain that your input is correct:
 
 ```rust
 // This could be provided by the email application.
@@ -104,9 +104,9 @@ let tag = TagGenerator::random();
 Command {
     tag,
     body: CommandBody::Login {
-        // Note that the "unchecked" feature must be activated.
-        username: AString::from(Atom::unchecked("alice")),
-        password: Secret::new(AString::from(Atom::unchecked("password"))),
+        // Note that the "unvalidated" feature must be activated.
+        username: AString::from(Atom::unvalidated("alice")),
+        password: Secret::new(AString::from(Atom::unvalidated("password"))),
     },
 };
 ```
diff --git a/imap-types/fuzz/Cargo.toml b/imap-types/fuzz/Cargo.toml
index 8faeeeea..65143f42 100644
--- a/imap-types/fuzz/Cargo.toml
+++ b/imap-types/fuzz/Cargo.toml
@@ -8,11 +8,12 @@ edition = "2021"
 [package.metadata]
 cargo-fuzz = true
 
-# Forward features according to ...
-#    https://rust-fuzz.github.io/book/cargo-fuzz/guide.html#cargo-features
 [features]
-debug = []
+# <Forward to imap-codec>
+# IMAP
 starttls = ["imap-types/starttls"]
+
+# IMAP Extensions
 ext_compress = ["imap-types/ext_compress"]
 ext_condstore_qresync = ["imap-types/ext_condstore_qresync"]
 ext_enable = ["imap-types/ext_enable"]
@@ -24,6 +25,8 @@ ext_move = ["imap-types/ext_move"]
 ext_quota = ["imap-types/ext_quota"]
 ext_sasl_ir = ["imap-types/ext_sasl_ir"]
 ext_unselect = ["imap-types/ext_unselect"]
+# </Forward to imap-codec>
+
 # Use (most) IMAP extensions.
 ext = [
     "starttls",
@@ -39,6 +42,8 @@ ext = [
     "ext_sasl_ir",
     "ext_unselect",
 ]
+# Enable `Debug`-printing during parsing. This is useful to analyze crashes.
+debug = []
 
 [dependencies]
 bounded-static = "0.5.0"
diff --git a/imap-types/src/arbitrary.rs b/imap-types/src/arbitrary.rs
index e62b8b6c..89d3caeb 100644
--- a/imap-types/src/arbitrary.rs
+++ b/imap-types/src/arbitrary.rs
@@ -1,6 +1,8 @@
 use arbitrary::{Arbitrary, Unstructured};
 use chrono::{FixedOffset, TimeZone};
 
+#[cfg(feature = "ext_literal")]
+use crate::core::LiteralMode;
 #[cfg(feature = "ext_enable")]
 use crate::extensions::enable::CapabilityEnableOther;
 #[cfg(feature = "ext_quota")]
@@ -12,8 +14,8 @@ use crate::{
         SinglePartExtensionData, SpecificFields,
     },
     core::{
-        AString, Atom, AtomExt, IString, Literal, LiteralMode, NString, NonEmptyVec, Quoted,
-        QuotedChar, Tag, Text,
+        AString, Atom, AtomExt, IString, Literal, NString, NonEmptyVec, Quoted, QuotedChar, Tag,
+        Text,
     },
     datetime::{DateTime, NaiveDate},
     envelope::Envelope,
diff --git a/imap-types/src/mailbox.rs b/imap-types/src/mailbox.rs
index 7495953a..4f60ce79 100644
--- a/imap-types/src/mailbox.rs
+++ b/imap-types/src/mailbox.rs
@@ -270,7 +270,9 @@ mod tests {
     use std::borrow::Cow;
 
     use super::*;
-    use crate::core::{AString, IString, Literal, LiteralMode};
+    #[cfg(feature = "ext_literal")]
+    use crate::core::LiteralMode;
+    use crate::core::{AString, IString, Literal};
 
     #[test]
     fn test_conversion_mailbox() {
diff --git a/imap-types/src/secret.rs b/imap-types/src/secret.rs
index f38d1798..c35acaef 100644
--- a/imap-types/src/secret.rs
+++ b/imap-types/src/secret.rs
@@ -1,12 +1,9 @@
 //! Handling of secret values.
 //!
-//! This module provides a `Secret<T>` ensuring that secret values are neither logged nor compared
-//! in non-constant time.
+//! This module provides a `Secret<T>` ensuring that sensitive values are not
+//! `Debug`-printed by accident.
 
-use std::{
-    borrow::Cow,
-    fmt::{Debug, Formatter},
-};
+use std::fmt::{Debug, Formatter};
 
 #[cfg(feature = "arbitrary")]
 use arbitrary::Arbitrary;
@@ -14,18 +11,15 @@ use arbitrary::Arbitrary;
 use bounded_static::ToStatic;
 #[cfg(feature = "serde")]
 use serde::{Deserialize, Serialize};
-use subtle::ConstantTimeEq;
 
-use crate::core::{AString, IString, Literal};
-
-/// A wrapper to ensure that secrets are neither logged nor compared in non-constant time.
+/// A wrapper to ensure that secrets are redacted during `Debug`-printing.
 #[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
 #[cfg_attr(feature = "bounded-static", derive(ToStatic))]
 #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
 // Note: The implementation of these traits does agree:
 //       `PartialEq` is just a thin wrapper that ensures constant-time comparison.
 #[allow(clippy::derived_hash_with_manual_eq)]
-#[derive(Clone, Hash)]
+#[derive(Clone, Eq, Hash, PartialEq)]
 pub struct Secret<T>(T);
 
 impl<T> Secret<T> {
@@ -34,7 +28,7 @@ impl<T> Secret<T> {
         Self(inner)
     }
 
-    /// Expose the inner secret (opting-out of all guarantees).
+    /// Expose the inner secret.
     pub fn declassify(&self) -> &T {
         &self.0
     }
@@ -46,32 +40,6 @@ impl<T> From<T> for Secret<T> {
     }
 }
 
-impl<T> Secret<T>
-where
-    T: AsRef<[u8]>,
-{
-    /// Compare this secret value with another value in constant time.
-    ///
-    /// Note: The comparison is made by converting both values as bytes first.
-    pub fn compare_with<B>(&self, other: B) -> bool
-    where
-        B: AsRef<[u8]>,
-    {
-        self.declassify().as_ref().ct_eq(other.as_ref()).unwrap_u8() == 1
-    }
-}
-
-impl<T> PartialEq for Secret<T>
-where
-    T: CompareCT<T>,
-{
-    fn eq(&self, other: &Self) -> bool {
-        self.declassify().compare_ct(&other.0)
-    }
-}
-
-impl<T> Eq for Secret<T> where T: CompareCT<T> {}
-
 impl<T> Debug for Secret<T>
 where
     T: Debug,
@@ -84,75 +52,8 @@ where
     }
 }
 
-/// A type that implements this trait can be compared in constant time.
-pub trait CompareCT<T> {
-    /// Compare in constant time.
-    #[must_use]
-    fn compare_ct(&self, other: &T) -> bool;
-}
-
-impl<'a, T> CompareCT<T> for Cow<'a, [u8]>
-where
-    T: AsRef<[u8]>,
-{
-    fn compare_ct(&self, other: &T) -> bool {
-        self.as_ref().ct_eq(other.as_ref()).unwrap_u8() == 1
-    }
-}
-
-impl<T> CompareCT<T> for Vec<u8>
-where
-    T: AsRef<[u8]>,
-{
-    fn compare_ct(&self, other: &T) -> bool {
-        self.as_slice().ct_eq(other.as_ref()).unwrap_u8() == 1
-    }
-}
-
-impl<'a> CompareCT<AString<'a>> for AString<'a> {
-    fn compare_ct(&self, other: &AString<'a>) -> bool {
-        match (self, other) {
-            (AString::Atom(lhs), AString::Atom(rhs)) => {
-                lhs.as_ref()
-                    .as_bytes()
-                    .ct_eq(rhs.as_ref().as_bytes())
-                    .unwrap_u8()
-                    == 1
-            }
-            (AString::String(lhs), AString::String(rhs)) => lhs.compare_ct(rhs),
-            _ => false,
-        }
-    }
-}
-
-impl<'a> CompareCT<IString<'a>> for IString<'a> {
-    fn compare_ct(&self, other: &IString<'a>) -> bool {
-        match (self, other) {
-            (IString::Quoted(lhs), IString::Quoted(rhs)) => {
-                lhs.as_ref()
-                    .as_bytes()
-                    .ct_eq(rhs.as_ref().as_bytes())
-                    .unwrap_u8()
-                    == 1
-            }
-            (IString::Literal(lhs), IString::Literal(rhs)) => lhs.compare_ct(rhs),
-            _ => false,
-        }
-    }
-}
-
-impl<'a> CompareCT<Literal<'a>> for Literal<'a> {
-    fn compare_ct(&self, other: &Literal<'a>) -> bool {
-        #[cfg(not(feature = "ext_literal"))]
-        return self.as_ref().ct_eq(other.as_ref()).unwrap_u8() == 1;
-        #[cfg(feature = "ext_literal")]
-        return self.as_ref().ct_eq(other.as_ref()).unwrap_u8() == 1 && self.mode == other.mode;
-    }
-}
-
 #[cfg(test)]
 mod tests {
-    use super::*;
     #[cfg(feature = "ext_literal")]
     use crate::core::Literal;
     use crate::{
@@ -164,6 +65,7 @@ mod tests {
     #[cfg(not(debug_assertions))]
     #[allow(clippy::redundant_clone)]
     fn test_that_secret_is_redacted() {
+        use super::Secret;
         #[cfg(feature = "ext_sasl_ir")]
         use crate::auth::AuthMechanism;
         use crate::auth::AuthenticateData;
@@ -212,85 +114,6 @@ mod tests {
         }
     }
 
-    /// A best effort test to ensure that constant-time comparison works.
-    #[test]
-    #[allow(clippy::redundant_clone)]
-    fn test_that_eq_is_constant_time() {
-        let took_constant = {
-            fn compare_eq(a: Secret<AString>, b: Secret<AString>) -> u128 {
-                let tik = std::time::Instant::now();
-                assert_eq!(a, b);
-                let tok = std::time::Instant::now();
-
-                tok.duration_since(tik).as_nanos()
-            }
-
-            fn compare_ne(a: Secret<AString>, b: Secret<AString>) -> u128 {
-                let tik = std::time::Instant::now();
-                assert_ne!(a, b);
-                let tok = std::time::Instant::now();
-
-                tok.duration_since(tik).as_nanos()
-            }
-
-            let a = Secret::new(AString::from(
-                Atom::try_from(str::repeat("A", 1024 * 1024)).unwrap(),
-            ));
-            let b = Secret::new(AString::from(
-                Atom::try_from(str::repeat("B", 1024 * 1024)).unwrap(),
-            ));
-
-            let took1 = compare_eq(a.clone(), a.clone());
-            println!("{}", took1);
-            let took2 = compare_ne(a.clone(), b.clone());
-            println!("{}", took2);
-            let took3 = compare_ne(b.clone(), a.clone());
-            println!("{}", took3);
-            let took4 = compare_eq(b.clone(), b.clone());
-            println!("{}", took4);
-
-            (took1 + took2 + took3 + took4) / 4
-        };
-
-        let took_variable = {
-            fn compare_eq(a: String, b: String) -> u128 {
-                let tik = std::time::Instant::now();
-                assert_eq!(a, b);
-                let tok = std::time::Instant::now();
-
-                tok.duration_since(tik).as_nanos()
-            }
-
-            fn compare_ne(a: String, b: String) -> u128 {
-                let tik = std::time::Instant::now();
-                assert_ne!(a, b);
-                let tok = std::time::Instant::now();
-
-                tok.duration_since(tik).as_nanos()
-            }
-
-            let a = str::repeat("A", 1024 * 1024);
-            let b = str::repeat("B", 1024 * 1024);
-
-            let took1 = compare_eq(a.clone(), a.clone());
-            println!("{}", took1);
-            let took2 = compare_ne(a.clone(), b.clone());
-            println!("{}", took2);
-            let took3 = compare_ne(b.clone(), a.clone());
-            println!("{}", took3);
-            let took4 = compare_eq(b.clone(), b.clone());
-            println!("{}", took4);
-
-            (took1 + took2 + took3 + took4) / 4
-        };
-
-        let times = took_constant / took_variable;
-        println!("{took_constant} vs {took_variable} ({times} times slower)");
-        if times < 10 {
-            panic!("expected slowdown >= 10, got {}", times);
-        }
-    }
-
     #[test]
     fn test_that_secret_has_no_side_effects_on_eq() {
         assert_ne!(
diff --git a/src/codec.rs b/src/codec.rs
index d758fbe0..3b4dc2f7 100644
--- a/src/codec.rs
+++ b/src/codec.rs
@@ -45,9 +45,9 @@ mod encode;
 mod tests {
     use std::num::NonZeroU32;
 
-    use imap_types::core::LiteralMode;
-
     use super::*;
+    #[cfg(feature = "ext_literal")]
+    use crate::core::LiteralMode;
     use crate::{
         command::{Command, CommandBody},
         core::{IString, Literal, NString, NonEmptyVec},
diff --git a/src/codec/decode.rs b/src/codec/decode.rs
index 09374b40..4f9cd2af 100644
--- a/src/codec/decode.rs
+++ b/src/codec/decode.rs
@@ -1,5 +1,6 @@
 use std::num::{ParseIntError, TryFromIntError};
 
+#[cfg(feature = "ext_literal")]
 use imap_types::core::LiteralMode;
 use nom::error::{ErrorKind, FromExternalError, ParseError};
 
diff --git a/src/codec/encode.rs b/src/codec/encode.rs
index 4c288693..aef68d59 100644
--- a/src/codec/encode.rs
+++ b/src/codec/encode.rs
@@ -2,6 +2,7 @@ use std::{io::Write, num::NonZeroU32};
 
 use base64::{engine::general_purpose::STANDARD as base64, Engine};
 use chrono::{DateTime as ChronoDateTime, FixedOffset};
+#[cfg(feature = "ext_literal")]
 use imap_types::core::LiteralMode;
 use utils::{join_serializable, List1AttributeValueOrNil, List1OrNil};
 
diff --git a/src/lib.rs b/src/lib.rs
index a8e93606..d53a0248 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -55,11 +55,12 @@
 //! ### Example
 //!
 //! ```rust
+//! #[cfg(feature = "ext_literal")]
+//! use imap_codec::core::LiteralMode;
 //! use imap_codec::{
 //!     codec::{Decode, Encode, Fragment},
-//!     command::Command,
+//!     command::{Command, CommandBody},
 //! };
-//! use imap_types::{command::CommandBody, core::LiteralMode};
 //!
 //! let command = Command::new("A1", CommandBody::login("Alice", "Pa²²W0rD").unwrap()).unwrap();
 //!
@@ -140,8 +141,8 @@
 //! The "tokio_util_compat" feature unlocks an implementation of [tokio_util::codec](https://docs.rs/tokio-util/latest/tokio_util/codec/index.html).
 //! See the [tokio client] and [tokio server] demos.
 //!
-//! [tokio client]: https://github.com/duesee/imap-codec/tree/main/assets/demos/tokio_client
-//! [tokio server]: https://github.com/duesee/imap-codec/tree/main/assets/demos/tokio_server
+//! [tokio client]: https://github.com/duesee/imap-codec/tree/main/assets/demos/tokio-client
+//! [tokio server]: https://github.com/duesee/imap-codec/tree/main/assets/demos/tokio-server
 //! [tokio_util::codec]: https://docs.rs/tokio-util/latest/tokio_util/codec/index.html
 //! [parse_command]: https://github.com/duesee/imap-codec/blob/main/examples/parse_command.rs
 //! [RFC 2088]: https://datatracker.ietf.org/doc/html/rfc2088


Test patch:
diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml
index 44e52c55..1ba8be23 100644
--- a/.github/workflows/build_and_test.yml
+++ b/.github/workflows/build_and_test.yml
@@ -106,8 +106,8 @@ jobs:
       - name: Check
         run: |
           cargo +nightly update -Z minimal-versions
-          cargo +1.60 check --workspace --all-targets --all-features
-          cargo +1.60 test --workspace --all-targets --all-features --exclude imap-codec-fuzz --exclude imap-types-fuzz
+          cargo +1.60 check --workspace --all-targets --all-features --exclude tokio-server
+          cargo +1.60 test --workspace --all-targets --all-features --exclude tokio-server --exclude imap-codec-fuzz --exclude imap-types-fuzz
         env:
           RUSTFLAGS: -Dwarnings