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

[PM-10941] Add ssh import #82

Merged
merged 28 commits into from
Dec 19, 2024
Merged

[PM-10941] Add ssh import #82

merged 28 commits into from
Dec 19, 2024

Conversation

quexten
Copy link
Contributor

@quexten quexten commented Dec 16, 2024

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-10941

📔 Objective

Adds ssh import to the sdk so we can validate, decrypt, and convert keys on import on all clients, and unblock importing 1Password keys from pux exports.

This much cleans up the implementation of the desktop_native module, replacing a lot of mapping and matching code.

Further, this moves generator out of bitwarden-ssh's lib.rs.

Note: importer will subsequently be removed from desktop_native

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation
    team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed
    issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

Copy link
Contributor

github-actions bot commented Dec 16, 2024

Logo
Checkmarx One – Scan Summary & Detailsccb25fe9-9e8d-4226-b83f-c2e35a8cf0f4

No New Or Fixed Issues Found

Copy link

codecov bot commented Dec 16, 2024

Codecov Report

Attention: Patch coverage is 89.44954% with 23 lines in your changes missing coverage. Please review.

Project coverage is 64.75%. Comparing base (2c22c4f) to head (8d70e02).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
crates/bitwarden-wasm-internal/src/ssh.rs 0.00% 10 Missing ⚠️
crates/bitwarden-ssh/src/generator.rs 86.53% 7 Missing ⚠️
crates/bitwarden-ssh/src/import.rs 97.91% 3 Missing ⚠️
crates/bitwarden-ssh/src/error.rs 0.00% 2 Missing ⚠️
crates/bitwarden-ssh/src/lib.rs 90.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #82      +/-   ##
==========================================
+ Coverage   64.41%   64.75%   +0.33%     
==========================================
  Files         188      190       +2     
  Lines       13819    13979     +160     
==========================================
+ Hits         8902     9052     +150     
- Misses       4917     4927      +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@quexten quexten requested a review from dani-garcia December 16, 2024 12:01
@quexten quexten marked this pull request as ready for review December 16, 2024 12:01
Copy link
Member

@Hinton Hinton left a comment

Choose a reason for hiding this comment

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

In bitwarden-exporters we store test resources under resources. You probably want to copy the excluded instruction from the Cargo.toml to avoid the test resources being published to crates.io.

crates/bitwarden-ssh/src/import.rs Fixed Show fixed Hide fixed
crates/bitwarden-ssh/src/import.rs Fixed Show fixed Hide fixed
crates/bitwarden-ssh/src/import.rs Fixed Show fixed Hide fixed
crates/bitwarden-ssh/src/import.rs Fixed Show fixed Hide fixed
crates/bitwarden-ssh/src/import.rs Fixed Show fixed Hide fixed
Copy link
Member

@Hinton Hinton left a comment

Choose a reason for hiding this comment

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

There are some clippy errors that needs to be resolved. We can improve the documentation on most public functions and constants. Afterwards it should be easier to review the actual code.

crates/bitwarden-ssh/Cargo.toml Outdated Show resolved Hide resolved
crates/bitwarden-ssh/Cargo.toml Outdated Show resolved Hide resolved
Copy link
Member

@Hinton Hinton left a comment

Choose a reason for hiding this comment

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

Alright, this is a bit of a long one 😅. Generally function wise it seems fine, but we have a few code cleanups we can do along the way to reduce code duplication, rely on slightly less lower level primitives in certain crates and better convey what's happening.

I've left a bunch of suggestions and issues along the way which should be fairly non controversial. But there are also a few thoughts which are more discussion topics.

crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
Comment on lines 54 to 58
let encrypted_private_key_info = EncryptedPrivateKeyInfo::from_der(der.as_bytes())
.map_err(|_| SshKeyImportError::ParsingError)?;
encrypted_private_key_info
.decrypt(password.as_bytes())
.map_err(|_| SshKeyImportError::WrongPassword)?
Copy link
Member

@Hinton Hinton Dec 17, 2024

Choose a reason for hiding this comment

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

thought: SecretDoc defines a helper, from_pkcs8_encrypted_pem which could be used. EncryptedPrivateKeyInfo is somewhat lower level in my opinion?

You would need to map the error differently though since you get the generic pkcs8 error but it should expose a DecryptFailed case.

We can go a step further and lift the SecretDocument::from_pem into the else statement since we would be decoding it from scratch and not need the der representation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

    let decrypted_der = if let Some(password) = password {
        SecretDocument::from_pkcs8_encrypted_pem(&encoded_key, password.as_bytes()).map_err(
            |err| match err {
                pkcs8::Error::EncryptedPrivateKey(pkcs5::Error::DecryptFailed) => {
                    SshKeyImportError::WrongPassword
                }
                _ => SshKeyImportError::ParsingError,
            },
        )?
    } else {
        SecretDocument::from_pkcs8_pem(&encoded_key).map_err(|_| SshKeyImportError::ParsingError)?
    };

Like this?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, that seems nice. We could probably even rename it to der now that we don't have a regular der.

Copy link
Member

@audreyality audreyality Dec 18, 2024

Choose a reason for hiding this comment

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

SecretDocument::from_pkcs8_encrypted_pem and SecretDocument::from_pkcs8_pem have the same result type. That allows you to move the error handling out of the nested code block.

Something like:

let der = if let Some(password) = password {
    SecretDocument::from_pkcs8_encrypted_pem(&encoded_key, password.as_bytes())
} else {
    SecretDocument::from_pkcs8_pem(&encoded_key)
};

let der = match.map_err(|err| match err {
    pkcs8::Error::EncryptedPrivateKey(pkcs5::Error::DecryptFailed) =>
        SshKeyImportError::WrongPassword,
    _ => SshKeyImportError::ParsingError
})?;

It works as a one-liner if you want it to, but that feels a bit more cluttered. Splitting out the error handling from the control flow makes the purpose of each block clear and makes the error propagation stand out a bit more.

Rebinding the variable (let der) might seem a bit odd, but it's a common idiom when you're changing a type but not the underlying data. In this case, the first der is a Result<T> and it's unwrapped to a T, which fits the idiom exactly.

Copy link
Member

Choose a reason for hiding this comment

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

Agree, that does look cleaner.

crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
@quexten quexten requested a review from audreyality December 17, 2024 16:53
crates/bitwarden-ssh/src/import.rs Fixed Show fixed Hide fixed
crates/bitwarden-ssh/src/import.rs Fixed Show fixed Hide fixed
crates/bitwarden-ssh/src/import.rs Fixed Show fixed Hide fixed
@quexten quexten requested a review from Hinton December 18, 2024 14:13
Copy link
Member

@audreyality audreyality left a comment

Choose a reason for hiding this comment

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

Most of my feedback is advice that you can take or leave. Issues that block approval are marked with ⚠️.

Comment on lines +15 to +24
pub enum SshKeyImportError {
#[error("Failed to parse key")]
ParsingError,
#[error("Password required")]
PasswordRequired,
#[error("Wrong password")]
WrongPassword,
#[error("Unsupported key type")]
UnsupportedKeyType,
}
Copy link
Member

Choose a reason for hiding this comment

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

@Hinton - Is there a path forward for i18n in the SDK? These seem like the kind of errors we'd like to forward to the user, which would mean communicating the i18n key we'd like the UI to use for localization along with the error.

Copy link
Member

@Hinton Hinton Dec 19, 2024

Choose a reason for hiding this comment

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

No we leave that to the presentation layer for now. Using i18n in lower level services have always been sort of a crutch.

For WASM we support https://sdk-api-docs.bitwarden.com/bitwarden_error_macro/attr.bitwarden_error.html which exposes basic, flat and full. For non user facing errors it's safe to use basic or flat and log it. For user facing errors we generally encourage you to use flat or full, and properly handle the error in the presentation layer.

In this case if we want to show error messages to the user we should use flat or full and capture the different variants in the JS world and apply localization there.

Note: The story on mobile is slightly different due to some uniffi limitations. We're working on providing the same capabilities there.

Copy link
Member

@audreyality audreyality Dec 19, 2024

Choose a reason for hiding this comment

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

Thanks! I'm going to leave this item open for now, but it's not blocking the review.

crates/bitwarden-ssh/src/generator.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/generator.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/import.rs Show resolved Hide resolved
}
}

fn import_pkcs8_key(
Copy link
Member

Choose a reason for hiding this comment

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

🤔 There's a lot of repetitive map_err calls in this method, which leads me to believe that there's a better way to represent this logic.

For example, extracting the parsing out from the processing of the key would let the parser signal a wide number of error conditions. The importer can then map those parsing failures once as a failed parse.

let private_key: ed25519::KeypairBytes = private_key_info
.try_into()
.map_err(|_| SshKeyImportError::ParsingError)?;
ssh_key::private::PrivateKey::from(Ed25519Keypair::from(&private_key.secret_key.into()))
Copy link
Member

Choose a reason for hiding this comment

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

🤔 Three type conversions on a single line leads me to believe that there should be a more direct way to convert key formats. It looks like all of these types come from a library? If so, consider wrapping the logic in a newtype to implement an external trait on an external type.

@quexten quexten removed the request for review from dani-garcia December 18, 2024 17:14
audreyality
audreyality previously approved these changes Dec 18, 2024
Copy link
Member

@audreyality audreyality left a comment

Choose a reason for hiding this comment

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

My blocking concerns have been addressed. I'm leaving the non-blocking concerns and questions unresolved.

crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
Comment on lines 54 to 58
let encrypted_private_key_info = EncryptedPrivateKeyInfo::from_der(der.as_bytes())
.map_err(|_| SshKeyImportError::ParsingError)?;
encrypted_private_key_info
.decrypt(password.as_bytes())
.map_err(|_| SshKeyImportError::WrongPassword)?
Copy link
Member

Choose a reason for hiding this comment

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

Agree, that does look cleaner.

Copy link
Member

@Hinton Hinton left a comment

Choose a reason for hiding this comment

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

Thanks for iterating on this! Looks much better, some minor suggestions and we should be good.

Comment on lines 96 to 110
if private_key.is_encrypted() {
if let Some(password) = password {
private_key
.decrypt(password.as_bytes())
.map_err(|_| SshKeyImportError::WrongPassword)?
.try_into()
.map_err(|_| SshKeyImportError::ParsingError)
} else {
Err(SshKeyImportError::PasswordRequired)
}
} else {
private_key
.try_into()
.map_err(|_| SshKeyImportError::ParsingError)
}
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: We can improve this code slightly.

  • The if condition for password can be rewritten using ok_or which converts an Option to Result.
  • We can remove some code duplication by assigning a temporary variable and lifting out some code from the if block.
Suggested change
if private_key.is_encrypted() {
if let Some(password) = password {
private_key
.decrypt(password.as_bytes())
.map_err(|_| SshKeyImportError::WrongPassword)?
.try_into()
.map_err(|_| SshKeyImportError::ParsingError)
} else {
Err(SshKeyImportError::PasswordRequired)
}
} else {
private_key
.try_into()
.map_err(|_| SshKeyImportError::ParsingError)
}
let private_key = if private_key.is_encrypted() {
let password = password.ok_or(SshKeyImportError::PasswordRequired)?;
private_key
.decrypt(password.as_bytes())
.map_err(|_| SshKeyImportError::WrongPassword)?
} else {
private_key
}
private_key
.try_into()
.map_err(|_| SshKeyImportError::ParsingError)

Copy link
Member

Choose a reason for hiding this comment

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

We might want to provide some documentation here as well, since this will be available in TS.

crates/bitwarden-ssh/src/import.rs Fixed Show fixed Hide fixed
crates/bitwarden-ssh/src/import.rs Fixed Show fixed Hide fixed
crates/bitwarden-ssh/src/import.rs Fixed Show fixed Hide fixed
@quexten quexten requested a review from Hinton December 19, 2024 11:26
Hinton
Hinton previously approved these changes Dec 19, 2024
crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/import.rs Outdated Show resolved Hide resolved
crates/bitwarden-ssh/src/generator.rs Outdated Show resolved Hide resolved
@quexten quexten merged commit 4ef0c74 into main Dec 19, 2024
39 checks passed
@quexten quexten deleted the km/pm-10941/ssh-import branch December 19, 2024 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants