Skip to content

Commit

Permalink
Implement backward compatibility with older profile handling (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
humblepenguinn committed Sep 29, 2024
1 parent bc08e2c commit b6b152d
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 34 deletions.
47 changes: 43 additions & 4 deletions src/crypto/gpg.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::process::{Command, Stdio};

#[cfg(target_family = "unix")]
use gpgme::{Context, Data, Protocol};
Expand All @@ -7,13 +9,10 @@ use gpgme::{Context, Data, Protocol};
use regex::Regex;
#[cfg(target_family = "windows")]
use std::collections::VecDeque;
#[cfg(target_family = "windows")]
use std::io::Write;
#[cfg(target_family = "windows")]
use std::process::{Command, Stdio};

use crate::crypto::EncryptionType;
use crate::error::{Error, Result};
use crate::utils;

// Bytes that identify the file as being encrypted using the `gpg` method
pub const IDENTITY_BYTES: &[u8] = b"-----GPG ENCRYPTED FILE-----";
Expand Down Expand Up @@ -159,6 +158,46 @@ impl EncryptionType for GPG {
}
}

impl GPG {
pub fn is_this_type_fallback(profile_name: &str) -> Result<bool> {
let profile_file_path = utils::get_profile_filepath(profile_name)?;

let mut gpg_process = Command::new("gpg")
.arg("--pinentry-mode")
.arg("loopback")
.arg("--passphrase-fd")
.arg("0")
.arg("--decrypt")
.arg(profile_file_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|err| Error::from(err))?;

{
let stdin = gpg_process
.stdin
.as_mut()
.ok_or_else(|| Error::Msg("Failed to open stdin".to_owned()))?;
writeln!(stdin, "{}", "passphrase")?; // Pass in a dummy passphrase
}

let output = gpg_process
.wait_with_output()
.map_err(|err| Error::from(err))?;

let stderr_output = String::from_utf8_lossy(&output.stderr);
let stdout_output = String::from_utf8_lossy(&output.stdout);

if stderr_output.contains("encrypted with") || stdout_output.contains("encrypted with") {
Ok(true)
} else {
Ok(false)
}
}
}

/// Get the GPG keys available on the system
///
/// There are two different implementations for Unix and Windows.
Expand Down
20 changes: 13 additions & 7 deletions src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ pub mod gpg;
pub use age::AGE;
pub use gpg::GPG;

use crate::error::{Error, Result};
use crate::{
error::{Error, Result},
utils,
};

/// Trait for encryption types
///
Expand Down Expand Up @@ -125,12 +128,15 @@ pub fn create_encryption_type(
///
/// println!("{}", encryption_type.as_string());
/// ```
pub fn get_encryption_type(encrypted_content: &[u8]) -> Result<Box<dyn EncryptionType>> {
let e_type = if GPG::is_this_type(encrypted_content) {
"gpg"
} else {
"age"
};
pub fn get_encryption_type(profile_name: &str) -> Result<Box<dyn EncryptionType>> {
let encrypted_content = utils::get_profile_content(profile_name)?;

let e_type =
if GPG::is_this_type(&encrypted_content) || GPG::is_this_type_fallback(profile_name)? {
"gpg"
} else {
"age"
};

create_encryption_type("".to_string(), e_type)
}
106 changes: 84 additions & 22 deletions src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ use std::collections::HashMap;
use std::{io::Write, path::PathBuf};

use chrono::NaiveDate;
use colored::Colorize;
use inquire::Confirm;
use serde::{Deserialize, Serialize};

use crate::utils::{get_configdir, truncate_identity_bytes};
use crate::utils::{self, get_configdir, truncate_identity_bytes};

use crate::crypto::EncryptionType;
use crate::error::{Error, Result};
Expand Down Expand Up @@ -387,43 +389,43 @@ impl Profile {
encryption_type,
}
}
/// Create a new profile object from the encrypted contents of a stored
/// profile file
///

/// Create a new profile object from a profile file stored on the system
///
/// This method is not meant to be used by the end user. It is recommended
/// to use the [load_profile](crate::load_profile) macro to load a profile.
/// This method is not meant to be used by the end user. It is recommended to
/// use the [load_profile](crate::load_profile) macro to load a profile.
///
/// # Parameters
/// - `name` - The name of the profile
/// - `encrypted_content` - The encrypted contents of the profile file
/// - `profile_name` - The name of the profile
/// - `encryption_type` - The encryption type used to encrypt the profile
///
/// `name` can either be the name of the profile or the absolute path to the
/// profile file.
/// `profile_name` can either be the name of the profile or the absolute path
/// to the profile file.
///
/// # Returns
/// - `Result<Profile>`: the profile object if the operation was successful or an error if it was not
///
/// # Examples
/// /// # Examples
/// ```
/// use envio::Profile;
///
/// let encrypted_content = envio::utils::get_profile_content("my-profile").unwrap();
///
/// let mut profile = match Profile::from_content("my-profile", &encrypted_content, envio::crypto::get_encryption_type(&encrypted_content).unwrap()) {
/// let profile_name = "my-profile";
/// let mut profile = match Profile::from(profile_name, envio::crypto::get_encryption_type(profile_name).unwrap()) {
/// Ok(p) => p,
/// Err(e) => {
/// eprintln!("An error occurred: {}", e);
/// return;
/// }
/// };
/// ```
pub fn from_content(
encrypted_content: &[u8],
encryption_type: Box<dyn EncryptionType>,
pub fn from(
profile_name: &str,
mut encryption_type: Box<dyn EncryptionType>,
) -> Result<Profile> {
let truncated_content = truncate_identity_bytes(encrypted_content);
let profile_file_path = utils::get_profile_filepath(profile_name)?;
let encrypted_content = std::fs::read(&profile_file_path)?;

let truncated_content = truncate_identity_bytes(&encrypted_content);

let content = match encryption_type.decrypt(&truncated_content) {
Ok(c) => c,
Expand All @@ -434,7 +436,68 @@ impl Profile {

match bincode::deserialize(&content) {
Ok(profile) => Ok(profile),
Err(e) => Err(Error::Deserialization(e.to_string())),
Err(_) => {
// Profiles created with older versions of envio are not serialized using bincode
println!(
"{}",
format!(
"{}: Unable to deserialize the profile content\n\
\n\
This may indicate:\n\
- The file has been tampered with\n\
- It was created with an older version of the tool\n",
"Warning".yellow().bold()
)
);

let prompt =
Confirm::new("Do you want to fallback to the old way of reading the profile?")
.with_default(false)
.with_help_message("If the file has been tampered with, then falling back to the old way of reading the profile will not work")
.prompt();

let fallback = match prompt {
Ok(f) => f,
Err(e) => return Err(Error::Msg(e.to_string())),
};

if !fallback {
return Err(Error::Deserialization(
"Unable to deserialize the profile content".to_string(),
));
}

let mut envs = HashMap::new();
let string_content = String::from_utf8_lossy(&content);
for line in string_content.lines() {
if line.is_empty() {
continue;
}

if !line.contains('=') {
encryption_type.set_key(line.to_string());
continue;
}

let mut parts = line.splitn(2, '=');
if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
envs.insert(key.to_string(), value.to_string());
}
}

let mut profile = Profile::new(
profile_name.to_owned(),
envs.into(),
profile_file_path,
encryption_type,
);

profile.push_changes()?; // Update the profile file with the new format

println!("{}", "Fallback successful!".green().bold());

return Ok(profile);
}
}
}

Expand Down Expand Up @@ -715,10 +778,9 @@ macro_rules! load_profile {
use envio::crypto;
use envio::utils;

let encrypted_content = utils::get_profile_content($name)?;
let mut encryption_type;

match crypto::get_encryption_type(&encrypted_content) {
match crypto::get_encryption_type($name) {
Ok(t) => encryption_type = t,
Err(e) => return Err(e.into()),
}
Expand All @@ -730,7 +792,7 @@ macro_rules! load_profile {
)?
}

match Profile::from_content(&encrypted_content, encryption_type) {
match Profile::from($name, encryption_type) {
Ok(profile) => return Ok(profile),
Err(e) => return Err(e.into()),
}
Expand Down
12 changes: 11 additions & 1 deletion src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,17 @@ pub fn get_configdir() -> PathBuf {
pub fn truncate_identity_bytes(encrypted_contents: &[u8]) -> Vec<u8> {
let mut truncated_contents = encrypted_contents.to_owned();

truncated_contents.truncate(encrypted_contents.len() - 28);
if encrypted_contents.len() < 28 {
return truncated_contents;
}

// check if the last 28 bytes are the identity bytes
if &truncated_contents[encrypted_contents.len() - 28..] == crate::crypto::age::IDENTITY_BYTES
|| &truncated_contents[encrypted_contents.len() - 28..]
== crate::crypto::gpg::IDENTITY_BYTES
{
truncated_contents.truncate(encrypted_contents.len() - 28);
}

truncated_contents
}

0 comments on commit b6b152d

Please sign in to comment.