-
-
Notifications
You must be signed in to change notification settings - Fork 35
Use secure_string for more secure Password and SecretKey #328
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
2cdfd65
8ee2f46
e55a961
0d29a72
c7dc276
e255fd1
c2d06c0
767a1f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,3 +13,6 @@ target/ | |
| *.db | ||
| *.sqlite3 | ||
| *.sqlite3-journal | ||
|
|
||
| # VSCode | ||
| .vscode/ | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,9 @@ use cot::db::impl_postgres::PostgresValueRef; | |
| #[cfg(feature = "sqlite")] | ||
| use cot::db::impl_sqlite::SqliteValueRef; | ||
| use email_address::EmailAddress; | ||
| #[cfg(not(miri))] | ||
| use secure_string::SecureString; | ||
| use subtle::ConstantTimeEq; | ||
|
|
||
| #[cfg(feature = "db")] | ||
| use crate::db::{ColumnType, DatabaseField, DbValue, FromDbValue, SqlxValueRef, ToDbValue}; | ||
|
|
@@ -45,30 +48,50 @@ const MAX_EMAIL_LENGTH: u32 = 254; | |
| /// compare it with the other password. This method uses constant-time | ||
| /// equality comparison, which protects against timing attacks. | ||
| /// | ||
| /// 2. An alternative is to use the [`Password::as_str`] method and compare the | ||
| /// strings directly. This approach uses non-constant-time comparison, which | ||
| /// is less secure but may be acceptable in certain legitimate use cases | ||
| /// where the security tradeoff is understood, e.g., when you're creating a | ||
| /// user registration form with the "retype your password" field, where both | ||
| /// passwords come from the same source anyway. | ||
| /// 2. An alternative is to compare 2 instances of the [`Password`] type | ||
| /// directly because this password struct implements the [`PartialEq`] trait | ||
| /// which also uses constant-time comparison. Comparing 2 instances of the | ||
| /// [`Password`] type is less secure than using [`PasswordHash::verify`], but | ||
| /// may be acceptable in certain legitimate use cases where the security | ||
| /// tradeoff is understood, e.g., when you're creating a user registration | ||
| /// form with the "retype your password" field, in this case this approach | ||
| /// might save on hashing costs. | ||
| /// | ||
| /// # Examples | ||
| /// | ||
| /// ``` | ||
| /// use cot::auth::Password; | ||
| /// | ||
| /// let password = Password::new("pass"); | ||
| /// assert_eq!(&format!("{:?}", password), "Password(\"**********\")"); | ||
| /// assert_eq!(&format!("{:?}", password), "Password(***SECRET***)"); | ||
| /// | ||
| /// let another_password = Password::new("pass"); | ||
| /// assert_eq!(password, another_password); | ||
| /// ``` | ||
| #[derive(Clone)] | ||
| pub struct Password(String); | ||
|
|
||
| impl Debug for Password { | ||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| f.debug_tuple("Password").field(&"**********").finish() | ||
| #[derive(Clone, Debug)] | ||
| #[cfg(not(miri))] | ||
| pub struct Password(SecureString); | ||
|
|
||
| impl PartialEq for Password { | ||
| fn eq(&self, other: &Self) -> bool { | ||
| self.as_str() | ||
| .as_bytes() | ||
| .ct_eq(other.as_str().as_bytes()) | ||
| .into() | ||
| } | ||
| } | ||
|
|
||
| impl Eq for Password {} | ||
|
|
||
| /// A password - simplified version for Miri testing. | ||
| /// | ||
| /// When running under Miri, we use a simple String wrapper instead of | ||
| /// SecureString to avoid the mlock system call that Miri doesn't support. | ||
| #[derive(Clone)] | ||
| #[cfg(miri)] | ||
| pub struct Password(String); | ||
|
|
||
| #[cfg(not(miri))] | ||
| impl Password { | ||
| /// Creates a new password object. | ||
| /// | ||
|
|
@@ -81,7 +104,7 @@ impl Password { | |
| /// ``` | ||
| #[must_use] | ||
| pub fn new<T: Into<String>>(password: T) -> Self { | ||
| Self(password.into()) | ||
| Self(SecureString::from(password.into())) | ||
| } | ||
|
|
||
| /// Returns the password as a string. | ||
|
|
@@ -96,7 +119,7 @@ impl Password { | |
| /// ``` | ||
| #[must_use] | ||
| pub fn as_str(&self) -> &str { | ||
| &self.0 | ||
| self.0.unsecure() | ||
| } | ||
|
|
||
| /// Consumes the object and returns the password as a string. | ||
|
|
@@ -110,6 +133,27 @@ impl Password { | |
| /// assert_eq!(password.into_string(), "password"); | ||
| /// ``` | ||
| #[must_use] | ||
| pub fn into_string(self) -> String { | ||
| self.0.into_unsecure() | ||
| } | ||
| } | ||
|
|
||
| #[cfg(miri)] | ||
| impl Password { | ||
| /// Creates a new password object - Miri version. | ||
| #[must_use] | ||
| pub fn new<T: Into<String>>(password: T) -> Self { | ||
| Self(password.into()) | ||
| } | ||
|
|
||
| /// Returns the password as a string - Miri version. | ||
| #[must_use] | ||
| pub fn as_str(&self) -> &str { | ||
| &self.0 | ||
| } | ||
|
|
||
| /// Consumes the object and returns the password as a string - Miri version. | ||
| #[must_use] | ||
| pub fn into_string(self) -> String { | ||
| self.0 | ||
| } | ||
|
|
@@ -133,6 +177,13 @@ impl From<String> for Password { | |
| } | ||
| } | ||
|
|
||
| #[cfg(miri)] | ||
| impl Debug for Password { | ||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| write!(f, "Password(***SECRET***)") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using non-rustic outputs like trailing
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for pointing that out, TIL about it! @eibrahim95 could you change it, please?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made it like this so it is the same as |
||
| } | ||
| } | ||
|
|
||
| /// A validated email address. | ||
| /// | ||
| /// This is a newtype wrapper around | ||
|
|
@@ -422,7 +473,21 @@ mod tests { | |
| #[test] | ||
| fn password_debug() { | ||
| let password = Password::new("password"); | ||
| assert_eq!(format!("{password:?}"), "Password(\"**********\")"); | ||
| assert_eq!(format!("{password:?}"), "Password(***SECRET***)"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn password_eq() { | ||
| let password1 = Password::new("password"); | ||
| let password2 = Password::new("password"); | ||
| assert_eq!(password1, password2); | ||
| } | ||
|
|
||
| #[test] | ||
| fn password_ne() { | ||
| let password1 = Password::new("password"); | ||
| let password2 = Password::new("password2"); | ||
| assert_ne!(password1, password2); | ||
| } | ||
|
|
||
| #[test] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,4 +20,5 @@ allow = [ | |
| "0BSD", | ||
| "BSD-3-Clause", | ||
| "Zlib", | ||
| "Unlicense", | ||
| ] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
secure-stringlooks kinda dead. Why not usezeroizedirectly?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
zeroizeis great at what it does, but it doesn't do enough.secure-string, apart from zeroing the memory, provides constant time equality (although as we looked before, it might just be claim not backed by code, I'll have to double check it) and protections against leaking into swap and core dumps. I believe that PR is good enough to merge it as is and we can always do some additional research and improve that part of the code later on, what do you think?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seqre this is untrue, as you (and I) pointed out. The PR uses
subtlefor this functionality.I somewhat agree with @Kijewski that
secure-stringis not very well maintained.zeroizeprovides a good portion of the functionality, butmlocking the memory would be nice. We could implement this ourselves (which shouldn't be very difficult, looking at the code), so perhaps it would be good to get this right from the beginning (or just ignore themlocking part for now). WDYT?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see a few solutions:
secure-string, update it, use that version, and try to upstream the improvements - it ain't perfect, but something like this: ISibboI/secure-string@main...seqre-contrib:secure-string:mainUNLICENSE, so we can just pull that code into Cot.The 2nd might be the best, we get what we want, and we might even improve the ecosystem by upstreaming improvements. This way, we can fairly quickly get what we want, and the only change required here is to change the source of the dependency. Would you agree @m4tx?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seqre ah fair, I guess we could use a fork. Do you mind transferring it to the cot-rs organization, so that it's clear for people that we definitely have the will to maintain it? Similarly to https://github.com/cot-rs/openapi-guis
If we want to use the fork, we'll probably want to publish it on crates.io - at this point I don't think we should use any
gitdependencies. Of course, ideally, as you say, we should upstream the changes, but if we don't get a response for say, a week or two, we should probably stick to our fork.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The repository has been transferred: https://github.com/cot-rs/secure-string
However, I think that using a git dependency, especially for a repo in our control, is fine for now, @m4tx. If we can't upstream the changes promptly, then sure, we can publish it to crates.io. Similarly to what we did before with other crates that we had to fix ourselves.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Issues were disabled on the forked repo, so I have copy comment here:
<SecureVec<T> as PartialEq>::eqis not saneI don't think the change in cot-rs/secure-string@004f3da is sane. For a
Twhich contains any internal or trailing padding, e.g.#[repr(C)] struct T { x: u8, y: u16 }, the bytes in the padding cannot be relied on, and should not be read. Also, genericTs might not have the same order as its transmuted[u8]slice.The simplest solution would be to remove the generic
SecureVec<T>, and invariantly setT = u8. Then you could concentrate on secure strings, without having to care about any and all types there could be.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seqre we can't release the crate to crates.io with a git dependency. When published, the git dependency will be replaced with a reference to a crates.io-published version, so it just won't work.
@Kijewski yeah, I agree that the implementation is not correct, thanks for vigilance! I think we can just use
[T]::ct_eq(it's aVec<T>underneath anyway) and remove the overcomplicated implementation and the unsafe code. SettingT = u8would also work. @seqre is there a reason you opted for this solution?I also re-enabled Issues on the repo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was the quickest thing I could do to show a proof of concept in a few minutes; it wasn't intended to be the final solution. If we want to go forward with that fork, then we should spend more time on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah sure, that makes sense!