Add support for storing encryption passkey in system keyring (#265)
- add keyring package - add keyring id field to manifest - automatically attempt to load encryption passkey from keyring - have decrypt delete the passkey on decrypt - have encrypt command ask if it should store the passkey in the keyring - fix lints closes #117
This commit is contained in:
parent
fe663cf43f
commit
7e94f76653
11 changed files with 982 additions and 59 deletions
907
Cargo.lock
generated
907
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -14,9 +14,10 @@ repository = "https://github.com/dyc3/steamguard-cli"
|
|||
license = "GPL-3.0-or-later"
|
||||
|
||||
[features]
|
||||
default = ["qr", "updater"]
|
||||
qr = ["qrcode"]
|
||||
updater = ["update-informer"]
|
||||
default = ["qr", "updater", "keyring"]
|
||||
qr = ["dep:qrcode"]
|
||||
updater = ["dep:update-informer"]
|
||||
keyring = ["dep:keyring"]
|
||||
|
||||
# [[bin]]
|
||||
# name = "steamguard-cli"
|
||||
|
@ -63,6 +64,7 @@ update-informer = { version = "1.0.0", optional = true, default-features = false
|
|||
phonenumber = "0.3"
|
||||
cbc = { version = "0.1.2", features = ["std"] }
|
||||
inout = { version = "0.1.3", features = ["std"] }
|
||||
keyring = { version = "2.0.4", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3"
|
||||
|
|
|
@ -88,6 +88,18 @@ impl AccountManager {
|
|||
self.passkey = passkey;
|
||||
}
|
||||
|
||||
pub fn keyring_id(&self) -> Option<&String> {
|
||||
self.manifest.keyring_id.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_keyring_id(&mut self, keyring_id: String) {
|
||||
self.manifest.keyring_id = Some(keyring_id);
|
||||
}
|
||||
|
||||
pub fn clear_keyring_id(&mut self) {
|
||||
self.manifest.keyring_id = None;
|
||||
}
|
||||
|
||||
/// Loads all accounts, and registers them.
|
||||
pub fn load_accounts(&mut self) -> anyhow::Result<(), ManifestAccountLoadError> {
|
||||
let mut accounts = vec![];
|
||||
|
|
|
@ -44,6 +44,7 @@ impl From<SdaManifest> for ManifestV1 {
|
|||
Self {
|
||||
version: 1,
|
||||
entries: sda.entries.into_iter().map(|e| e.into()).collect(),
|
||||
keyring_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ pub type ManifestEntry = ManifestEntryV1;
|
|||
pub struct ManifestV1 {
|
||||
pub version: u32,
|
||||
pub entries: Vec<ManifestEntry>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub keyring_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
@ -25,6 +27,7 @@ impl Default for ManifestV1 {
|
|||
Self {
|
||||
version: 1,
|
||||
entries: vec![],
|
||||
keyring_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ fn do_migrate(
|
|||
deserialize_manifest(buffer).map_err(MigrationError::ManifestDeserializeFailed)?;
|
||||
|
||||
if manifest.is_encrypted() && passkey.is_none() {
|
||||
return Err(MigrationError::MissingPasskey);
|
||||
return Err(MigrationError::MissingPasskey { keyring_id: None });
|
||||
} else if !manifest.is_encrypted() && passkey.is_some() {
|
||||
// no custom error because this is an edge case, mostly user error
|
||||
return Err(MigrationError::UnexpectedError(anyhow::anyhow!("A passkey was provided but the manifest is not encrypted. Aborting migration because it would encrypt the maFiles, and you probably didn't mean to do that.")));
|
||||
|
@ -84,7 +84,7 @@ fn backup_file(path: &Path) -> anyhow::Result<()> {
|
|||
#[derive(Debug, Error)]
|
||||
pub(crate) enum MigrationError {
|
||||
#[error("Passkey is required to decrypt manifest")]
|
||||
MissingPasskey,
|
||||
MissingPasskey { keyring_id: Option<String> },
|
||||
#[error("Failed to deserialize manifest: {0}")]
|
||||
ManifestDeserializeFailed(serde_path_to_error::Error<serde_json::Error>),
|
||||
#[error("IO error when upgrading manifest: {0}")]
|
||||
|
|
|
@ -14,6 +14,17 @@ where
|
|||
{
|
||||
fn execute(&self, _transport: T, manager: &mut AccountManager) -> anyhow::Result<()> {
|
||||
load_accounts_with_prompts(manager)?;
|
||||
|
||||
#[cfg(feature = "keyring")]
|
||||
if let Some(keyring_id) = manager.keyring_id() {
|
||||
match crate::encryption::clear_passkey(keyring_id.clone()) {
|
||||
Ok(_) => {
|
||||
info!("Cleared passkey from keyring");
|
||||
manager.clear_keyring_id();
|
||||
}
|
||||
Err(e) => warn!("Failed to clear passkey from keyring: {}", e),
|
||||
}
|
||||
}
|
||||
for mut entry in manager.iter_mut() {
|
||||
entry.encryption = None;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use log::*;
|
||||
|
||||
use crate::AccountManager;
|
||||
use crate::{tui, AccountManager};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -31,6 +31,31 @@ where
|
|||
error!("Passkeys do not match, try again.");
|
||||
}
|
||||
let passkey = passkey.map(SecretString::new);
|
||||
|
||||
#[cfg(feature = "keyring")]
|
||||
{
|
||||
if tui::prompt_char(
|
||||
"Would you like to store the passkey in your system keyring?",
|
||||
"yn",
|
||||
) == 'y'
|
||||
{
|
||||
let keyring_id = crate::encryption::generate_keyring_id();
|
||||
match crate::encryption::store_passkey(
|
||||
keyring_id.clone(),
|
||||
passkey.clone().unwrap(),
|
||||
) {
|
||||
Ok(_) => {
|
||||
info!("Stored passkey in keyring");
|
||||
manager.set_keyring_id(keyring_id);
|
||||
}
|
||||
Err(e) => warn!(
|
||||
"Failed to store passkey in keyring, continuing anyway: {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manager.submit_passkey(passkey);
|
||||
}
|
||||
manager.load_accounts()?;
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
use aes::cipher::block_padding::Pkcs7;
|
||||
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, InvalidLength, KeyIvInit};
|
||||
use aes::Aes256;
|
||||
use rand::Rng;
|
||||
use ring::pbkdf2;
|
||||
use ring::rand::SecureRandom;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "keyring")]
|
||||
mod keyring;
|
||||
|
||||
#[cfg(feature = "keyring")]
|
||||
pub use crate::encryption::keyring::*;
|
||||
|
||||
const SALT_LENGTH: usize = 8;
|
||||
const IV_LENGTH: usize = 16;
|
||||
|
||||
|
@ -158,6 +165,14 @@ impl From<std::io::Error> for EntryEncryptionError {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn generate_keyring_id() -> String {
|
||||
let rng = rand::thread_rng();
|
||||
rng.sample_iter(rand::distributions::Alphanumeric)
|
||||
.take(32)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
24
src/encryption/keyring.rs
Normal file
24
src/encryption/keyring.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
use keyring::Entry;
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
|
||||
const KEYRING_SERVICE: &str = "steamguard-cli";
|
||||
|
||||
pub fn init_keyring(keyring_id: String) -> keyring::Result<Entry> {
|
||||
Entry::new(KEYRING_SERVICE, &keyring_id)
|
||||
}
|
||||
|
||||
pub fn try_passkey_from_keyring(keyring_id: String) -> keyring::Result<Option<SecretString>> {
|
||||
let entry = init_keyring(keyring_id)?;
|
||||
let passkey = entry.get_password()?;
|
||||
Ok(Some(SecretString::new(passkey)))
|
||||
}
|
||||
|
||||
pub fn store_passkey(keyring_id: String, passkey: SecretString) -> keyring::Result<()> {
|
||||
let entry = init_keyring(keyring_id)?;
|
||||
entry.set_password(passkey.expose_secret())
|
||||
}
|
||||
|
||||
pub fn clear_passkey(keyring_id: String) -> keyring::Result<()> {
|
||||
let entry = init_keyring(keyring_id)?;
|
||||
entry.delete_password()
|
||||
}
|
29
src/main.rs
29
src/main.rs
|
@ -140,10 +140,22 @@ fn run(args: commands::Args) -> anyhow::Result<()> {
|
|||
accounts = a;
|
||||
break;
|
||||
}
|
||||
Err(MigrationError::MissingPasskey) => {
|
||||
Err(MigrationError::MissingPasskey { keyring_id }) => {
|
||||
if passkey.is_some() {
|
||||
error!("Incorrect passkey");
|
||||
}
|
||||
|
||||
#[cfg(feature = "keyring")]
|
||||
if let Some(keyring_id) = keyring_id {
|
||||
if passkey.is_none() {
|
||||
info!("Attempting to load encryption passkey from keyring");
|
||||
let entry = encryption::init_keyring(keyring_id)?;
|
||||
let raw = entry.get_password()?;
|
||||
passkey = Some(SecretString::new(raw));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let raw =
|
||||
rpassword::prompt_password_stdout("Enter encryption passkey: ")?;
|
||||
passkey = Some(SecretString::new(raw));
|
||||
|
@ -167,6 +179,19 @@ fn run(args: commands::Args) -> anyhow::Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "keyring")]
|
||||
if let Some(keyring_id) = manager.keyring_id() {
|
||||
if passkey.is_none() {
|
||||
info!("Attempting to load encryption passkey from keyring");
|
||||
match encryption::try_passkey_from_keyring(keyring_id.clone()) {
|
||||
Ok(k) => passkey = k,
|
||||
Err(e) => {
|
||||
warn!("Failed to load encryption passkey from keyring: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manager.submit_passkey(passkey);
|
||||
|
||||
loop {
|
||||
|
@ -219,7 +244,7 @@ fn run(args: commands::Args) -> anyhow::Result<()> {
|
|||
break;
|
||||
}
|
||||
Err(
|
||||
accountmanager::ManifestAccountLoadError::MissingPasskey
|
||||
accountmanager::ManifestAccountLoadError::MissingPasskey { .. }
|
||||
| accountmanager::ManifestAccountLoadError::IncorrectPasskey,
|
||||
) => {
|
||||
if manager.has_passkey() {
|
||||
|
|
Loading…
Reference in a new issue