Skip to content
Merged
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
23 changes: 11 additions & 12 deletions src/commands/filter.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use crate::crypto;
use crate::key;
use crate::repo;
use crate::key::Key;
use crate::repo::Repo;
use anyhow::{Context, Result};
use std::fs;
use std::io::{self, Read, Write};

/// Git clean filter: encrypt data from stdin and write to stdout
/// This filter is idempotent: clean(clean(data)) == clean(data)
/// If the input is already encrypted (has magic header), it passes through unchanged.
pub fn clean_filter(repo: &repo::Repo) -> Result<()> {
pub fn clean_filter(repo: &Repo) -> Result<()> {
let key = repo.load_key().context("Failed to load encryption key")?;
let input = read_stdin()?;
let output = apply_clean_filter(&key, &input)?;
Expand All @@ -21,7 +21,7 @@ pub fn clean_filter(repo: &repo::Repo) -> Result<()> {
/// Git smudge filter: decrypt data from stdin and write to stdout
/// This filter is idempotent: smudge(smudge(data)) == smudge(data)
/// If the input is already plaintext (no magic header), it passes through unchanged.
pub fn smudge_filter(repo: &repo::Repo) -> Result<()> {
pub fn smudge_filter(repo: &Repo) -> Result<()> {
let key = repo.load_key().context("Failed to load encryption key")?;
let input = read_stdin()?;
let output = apply_smudge_filter(&key, &input)?;
Expand All @@ -34,7 +34,7 @@ pub fn smudge_filter(repo: &repo::Repo) -> Result<()> {
/// Git diff textconv: decrypt file and write to stdout
/// Used by git diff to show decrypted content of encrypted files.
/// Takes a filename as argument (provided by git when using textconv).
pub fn diff_textconv(repo: &repo::Repo, filename: &str) -> Result<()> {
pub fn diff_textconv(repo: &Repo, filename: &str) -> Result<()> {
let key = repo.load_key().context("Failed to load encryption key")?;

// Read file
Expand Down Expand Up @@ -68,7 +68,7 @@ fn read_stdin() -> Result<Vec<u8>> {

/// Apply clean filter logic: encrypt plaintext, pass through encrypted data unchanged
/// This is idempotent: clean(clean(data)) == clean(data)
fn apply_clean_filter(key: &key::Key, input: &[u8]) -> Result<Vec<u8>> {
fn apply_clean_filter(key: &Key, input: &[u8]) -> Result<Vec<u8>> {
// Check if input is already encrypted using magic header
if crypto::is_encrypted(input) {
// Input is already encrypted, pass through unchanged
Expand All @@ -82,7 +82,7 @@ fn apply_clean_filter(key: &key::Key, input: &[u8]) -> Result<Vec<u8>> {

/// Apply smudge filter logic: decrypt encrypted data, pass through plaintext unchanged
/// This is idempotent: smudge(smudge(data)) == smudge(data)
fn apply_smudge_filter(key: &key::Key, input: &[u8]) -> Result<Vec<u8>> {
fn apply_smudge_filter(key: &Key, input: &[u8]) -> Result<Vec<u8>> {
// Check if input is encrypted using magic header
if !crypto::is_encrypted(input) {
// Input is already plaintext, pass through unchanged
Expand All @@ -99,16 +99,15 @@ fn apply_smudge_filter(key: &key::Key, input: &[u8]) -> Result<Vec<u8>> {
#[cfg(test)]
mod tests {
use super::*;
use crate::key;

/// Constant test key for deterministic testing
fn test_key() -> key::Key {
const TEST_KEY_BYTES: [u8; key::Key::KEY_SIZE] = [
fn test_key() -> Key {
const TEST_KEY_BYTES: [u8; Key::KEY_SIZE] = [
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67,
0x89, 0xab, 0xcd, 0xef,
];
key::Key::from_bytes(TEST_KEY_BYTES)
Key::from(TEST_KEY_BYTES)
}

#[test]
Expand Down Expand Up @@ -263,7 +262,7 @@ mod tests {
#[test]
fn test_apply_smudge_filter_wrong_key_fails() {
let key1 = test_key();
let key2 = key::Key::generate().unwrap();
let key2 = Key::generate().unwrap();
let plaintext = b"Secret data";

let encrypted = crypto::encrypt(&key1, plaintext).unwrap();
Expand Down
19 changes: 9 additions & 10 deletions src/commands/init.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::key;
use crate::key::Key;
use crate::repo;
use crate::BINARY_NAME;
use anyhow::{Context, Result};
Expand All @@ -19,37 +19,36 @@ pub fn cmd_init() -> Result<()> {
}

// Generate a new key
let key = key::Key::generate().context("Failed to generate encryption key")?;
let key = Key::generate().context("Failed to generate encryption key")?;
repo.store_key(&key).context("Failed to store key file")?;

// Set up Git filters
repo.setup_filters()
.context("Failed to set up Git filters")?;

let key_b64 = key.to_base64();
let instructions = init_instructions(&key_b64);
let instructions = init_instructions(&key);
println!("{}", instructions);

Ok(())
}

/// Format initialization instructions for display to the user
fn init_instructions(key_b64: &str) -> String {
fn init_instructions(key: &Key) -> String {
format!(
indoc! {r#"
Repository initialized for {bin_name}

Your encryption key (base64, save this securely!):
{key_b64}
{key}

Once you share this key with users you trust, they can unlock their working copy using one of these methods:
- From base64-encoded key passed directly as argument:
{bin_name} unlock "{key_b64}"
{bin_name} unlock "{key}"
- From environment variable (base64):
export GIT_CONCEAL_SECRET_KEY='{key_b64}'
export GIT_CONCEAL_SECRET_KEY='{key}'
{bin_name} unlock env:GIT_CONCEAL_SECRET_KEY
- From stdin (raw binary, 32 bytes):
echo '{key_b64}' | base64 -d | {bin_name} unlock -
echo '{key}' | base64 -d | {bin_name} unlock -
{bin_name} unlock - < /path/to/raw-binary-key.bin

To start adding files to be encrypted in this repository:
Expand All @@ -63,7 +62,7 @@ fn init_instructions(key_b64: &str) -> String {
- Run '{bin_name} status' to validate the list of files that are encrypted.
"#},
bin_name = BINARY_NAME,
key_b64 = key_b64,
key = key,
filter = repo::FILTER_NAME,
diff = repo::DIFF_NAME,
)
Expand Down
17 changes: 8 additions & 9 deletions src/commands/key.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::key;
use crate::key::Key;
use crate::repo;
use crate::BINARY_NAME;
use anyhow::{Context, Result};
Expand All @@ -17,10 +17,10 @@ pub fn cmd_key_show(raw: bool) -> Result<()> {
let key = repo.load_key().context("Failed to load encryption key")?;
if raw {
std::io::stdout()
.write_all(key.as_bytes())
.write_all(key.as_ref())
.context("Failed to write key to stdout")?;
} else {
println!("{}", key.to_base64());
println!("{}", key);
}

Ok(())
Expand All @@ -39,7 +39,7 @@ pub fn cmd_key_rotate(skip_confirmation: bool) -> Result<()> {
anyhow::bail!("Key rotation cancelled.");
}

let new_key = key::Key::generate().context("Failed to generate new encryption key")?;
let new_key = Key::generate().context("Failed to generate new encryption key")?;
repo.store_key(&new_key)
.context("Failed to store new key")?;

Expand All @@ -49,8 +49,7 @@ pub fn cmd_key_rotate(skip_confirmation: bool) -> Result<()> {
.context("Failed to re-normalize encrypted files")?;

// Print follow-up instructions for the user
let new_key_b64 = new_key.to_base64();
let instructions = rotate_instructions(&new_key_b64);
let instructions = rotate_instructions(&new_key);
println!("{}", instructions);

Ok(())
Expand Down Expand Up @@ -100,14 +99,14 @@ fn rotate_confirmation_prompt() -> String {
}

/// Format key rotation instructions for display to the user
fn rotate_instructions(key_b64: &str) -> String {
fn rotate_instructions(key: &Key) -> String {
format!(
indoc! {r#"
Key rotation completed successfully
Encrypted file(s) have been re-keyed and staged for commit.

New encryption key (base64, save this securely and share with your team!):
{key_b64}
{key}

Next steps:
1. Consider also rotating the actual secrets contained in the secret files
Expand All @@ -125,6 +124,6 @@ fn rotate_instructions(key_b64: &str) -> String {
Once all team members have updated to the new key, the old key can be discarded.
"#},
bin_name = BINARY_NAME,
key_b64 = key_b64,
key = key,
)
}
8 changes: 4 additions & 4 deletions src/commands/status.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::repo;
use crate::repo::Repo;
use anyhow::{Context, Result};
use serde::Serialize;
use serde_json;
use std::fmt;
use std::path::PathBuf;

pub fn cmd_status(files: Vec<String>, json: bool) -> Result<()> {
let repo = repo::Repo::discover()?;
pub fn cmd_status<S: AsRef<str>>(files: &[S], json: bool) -> Result<()> {
let repo = Repo::discover()?;

if files.is_empty() {
// Show repository status
Expand Down Expand Up @@ -40,7 +40,7 @@ pub fn cmd_status(files: Vec<String>, json: bool) -> Result<()> {
let file_statuses: Vec<FileStatus> = files
.iter()
.map(|file_str| {
let file_path = std::path::Path::new(file_str);
let file_path = std::path::Path::new(file_str.as_ref());
let is_filtered = repo.is_filtered_file(file_path)?;
Ok(FileStatus {
file: file_path.to_path_buf(),
Expand Down
Loading