Skip to content
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

portal: Keyring::load() verify secret before returning a Keyring #122

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions client/src/portal/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,31 @@ impl Keyring {
)
}

pub(crate) fn verify_secret(&self, secret: &Secret) -> bool {
let key = self.derive_key(secret);

// if this is a new keyring or a keyring with zero items
if self.items.is_empty() {
return true;
}

// decrypting the first item may be enough to verify the secret.
// but to avoid a scenario where the first item is corrupted,
// decrypting all the items here.
let mut decrypt_counter = 0;
for item in self.items.clone() {
if item.decrypt(&key).is_ok() {
decrypt_counter += 1;
}
}

if decrypt_counter == self.items.len() {
return true;
}

false
}

// Reset Keyring content
pub(crate) fn reset(&mut self) {
let salt = rand::thread_rng().gen::<[u8; DEFAULT_SALT_SIZE]>().to_vec();
Expand Down
3 changes: 3 additions & 0 deletions client/src/portal/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub enum Error {
Utf8(std::str::Utf8Error),
/// Mismatch of algorithms used in legacy keyring file.
AlgorithmMismatch(u8),
/// Keyring secret mismatch
InvalidSecret,
}

impl From<zvariant::Error> for Error {
Expand Down Expand Up @@ -105,6 +107,7 @@ impl std::fmt::Display for Error {
Error::InvalidItemIndex(index) => write!(f, "The addressed item index {index} does not exist"),
Error::Utf8(e) => write!(f, "UTF-8 encoding error {e}"),
Error::AlgorithmMismatch(e) => write!(f, "Unknown algorithm {e}"),
Error::InvalidSecret => write!(f, "Keyring secret does not match the expected value"),
}
}
}
Expand Down
95 changes: 95 additions & 0 deletions client/src/portal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ impl Keyring {

let keyring = api::Keyring::try_from(content.as_slice())?;

if !keyring.verify_secret(&secret) {
return Err(Error::InvalidSecret);
}

(mtime, keyring)
}
};
Expand Down Expand Up @@ -450,6 +454,33 @@ impl Keyring {

self.write().await
}

/// Verify the secret associated with a keyring.
///
/// This function supports all keyring formats (legacy and latest).
///
/// # Arguments
/// *`name` - The name of the keyring.
/// * `secret` - The service key, usually retrieved from the Secrets portal.
pub async fn verify_secret(name: &str, secret: Secret) -> bool {
#[cfg(feature = "tracing")]
tracing::debug!("Trying to verify {} keyring secret", name);

let mut ret = false;

match Self::open(name, secret).await {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why I would open the keyring file a second time, when all of this verification the moment you call open.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what second time? :)

this is a new function: verify_secret. we need this for the server side implementation. specifically to verify the secret during Prompt based unlock. this function just validates the secret and it doesn't return a keyring.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load -> verify_secret -> open -> you end up opening reading the keyring twice. I am not sure the changes you made here even correct because on the other hand open would call load as well.... Not good.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason of your confusion, is there is a method load, that is used only when interacting with a secret that comes from the secret portal. So it is assumed to be always correct where open is what you would use in the server side.

The secret verification should be done to both cases just in case the API is misused.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load -> verify_secret -> open -> you end up opening reading the keyring twice. I am not sure the changes you made here even correct because on the other hand open would call load as well.... Not good.

Thanks. sorry for the late reply.

apologies if what I'm saying doesn't make any sense :)

so, when I call oo7::portal::Keyring::verify_secret() what happens is,

  • calling Keyring::open() right,
    • if the keyring is v0, Keyring::open() will call Keyring::migrate(). in this case the keyring file is opened once and the secret verification is taken care here. there isn't another call to Keyring::load().
    • if the keyring is v1 Keyring::open() will call Keyring::load(). in this case the keyring file is opened once and secret verification is done through verify_secret().

so, for both cases: Keyring::load(), Keyring::open() secret verification are covered now and the keyring file is opened once during the vertification. am I still missing something here?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant is:

  • If you use Keyring::open,

    • We verify if v1 exists -> call Keyring::load on it
    • if v0 exists, call Keyring::migrate which by itself verifies the secret as it decrypts the v0 items to migrate them to v1
  • So basically, by handling the verification in Keyring::load code path, you cover all the use cases.

Err(err) => {
if matches!(err, Error::InvalidSecret) {
ret = false;
}
}
Ok(_) => {
ret = true;
}
}

ret
}
}

#[cfg(test)]
Expand Down Expand Up @@ -679,6 +710,42 @@ mod tests {
Ok(())
}

#[tokio::test]
async fn load() -> Result<(), Error> {
let temp_dir = tempdir()?;
let keyring_dir = temp_dir.path().join("keyrings");

fs::create_dir_all(&keyring_dir).await?;

let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("fixtures")
.join("default.keyring");

fs::copy(&fixture_path, &keyring_dir.join("default.keyring")).await?;

std::env::set_var("XDG_DATA_HOME", &temp_dir.path());

let password = b"wrong";
let secret = Secret::from(password.to_vec());
match Keyring::load(keyring_dir.join("default.keyring"), secret).await {
Err(err) => {
assert!(matches!(err, Error::InvalidSecret));
}
Ok(_) => assert!(false),
};

let password = b"test";
let secret = Secret::from(password.to_vec());
match Keyring::load(keyring_dir.join("default.keyring"), secret).await {
Err(err) => {
assert!(matches!(err, Error::InvalidSecret));
}
Ok(_) => assert!(true),
};

Ok(())
}

#[tokio::test]
async fn change_secret() -> Result<(), Error> {
let path = PathBuf::from("../../tests/test_rekeying.keyring");
Expand All @@ -704,4 +771,32 @@ mod tests {

Ok(())
}

#[tokio::test]
async fn verify_secret() -> Result<(), Error> {
let temp_dir = tempdir()?;
let keyring_dir = temp_dir.path().join("keyrings");

fs::create_dir_all(&keyring_dir).await?;

let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("fixtures")
.join("legacy.keyring");

fs::copy(&fixture_path, &keyring_dir.join("legacy.keyring")).await?;

std::env::set_var("XDG_DATA_HOME", &temp_dir.path());

// test with a wrong secret
let password = b"wrong";
let secret = Secret::from(password.to_vec());
assert_eq!(Keyring::verify_secret("legacy", secret).await, false);

// test with the correct secret
let password = b"test";
let secret = Secret::from(password.to_vec());
assert!(Keyring::verify_secret("legacy", secret).await);

Ok(())
}
}