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

Implement running shell commands for getting passwords #315

Merged
merged 17 commits into from
Dec 18, 2022
Merged
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,23 +108,21 @@ join require identification. To use this method enter your nick password to the

### Using external commands for passwords

When a password field in the config file starts with `$`, tiny uses rest of the
field as the shell command to run to get the password.
When a password field in the config file is a map with a `cmd` key, the value
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we update the sample config.yml too?

Copy link
Owner Author

Choose a reason for hiding this comment

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

We show an example in the README so I'm not sure if it's necessary.

is used as the shell command to run to get the password.

For example, in this config:

```yaml
sasl:
username: osa1
password: '$ pass show "my irc password"'
password:
cmd: 'pass show "my irc password"'
```

tiny runs the `pass ...` command and uses last line printed by the command as
the password.

When your password starts with `$`, the initial `$` character can be escaped
with a `\`: `password: '\$myPass'`.

## Command line arguments

By default (i.e. when no command line arguments passed) tiny connects to all
Expand Down
42 changes: 18 additions & 24 deletions crates/tiny/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ where
pub(crate) enum PassOrCmd {
/// Password is given directly as plain text
Pass(String),

/// A shell command to run to get the password
Cmd(Vec<String>),
}
Expand All @@ -114,24 +115,23 @@ impl<'de> Deserialize<'de> for PassOrCmd {
D: serde::Deserializer<'de>,
{
use serde::de::Error;

let str = String::deserialize(deserializer)?;
let trimmed = str.trim();
if let Some(trimmed) = trimmed.strip_prefix('$') {
let args = match shell_words::split(trimmed) {
Ok(args) => args,
Err(err) => {
return Err(D::Error::custom(format!(
use serde_yaml::Value;

match Value::deserialize(deserializer)? {
Value::String(str) => Ok(PassOrCmd::Pass(str)),
Value::Mapping(map) => match map.get(&Value::String("cmd".to_owned())) {
Some(Value::String(cmd)) => match shell_words::split(cmd) {
Ok(cmd_parts) => Ok(PassOrCmd::Cmd(cmd_parts)),
Err(err) => Err(D::Error::custom(format!(
"Unable to parse password field: {}",
err
)))
}
};
Ok(PassOrCmd::Cmd(args))
} else if let Some(without_dollar) = trimmed.strip_prefix("\\$") {
Ok(PassOrCmd::Pass(format!("${}", without_dollar)))
} else {
Ok(PassOrCmd::Pass(str))
))),
},
_ => Err(D::Error::custom(
"Expeted a 'cmd' key in password map with string value",
)),
},
_ => Err(D::Error::custom("Password field must be a string or map")),
}
}
}
Expand Down Expand Up @@ -478,7 +478,7 @@ mod tests {

#[test]
fn parse_password_field() {
let field = "$ my pass cmd";
let field = "cmd: my pass cmd";
assert_eq!(
serde_yaml::from_str::<PassOrCmd>(&field).unwrap(),
PassOrCmd::Cmd(vec!["my".to_owned(), "pass".to_owned(), "cmd".to_owned()])
Expand All @@ -490,13 +490,7 @@ mod tests {
PassOrCmd::Pass(field.to_string())
);

let field = "\\$my password";
assert_eq!(
serde_yaml::from_str::<PassOrCmd>(&field).unwrap(),
PassOrCmd::Pass("$my password".to_string())
);

let field = "$ pass show \"my password\"";
let field = "cmd: \"pass show 'my password'\"";
assert_eq!(
serde_yaml::from_str::<PassOrCmd>(&field).unwrap(),
PassOrCmd::Cmd(vec![
Expand Down