fix(security): durcir les secrets locaux et valider les credentials

closes #4
This commit is contained in:
thibaud-lclr 2026-04-16 17:11:03 +02:00
parent 8d0b345751
commit 91459c16cc
3 changed files with 225 additions and 12 deletions

View file

@ -12,6 +12,10 @@ pub fn set_tuleap_credentials(
username: String,
password: String,
) -> Result<TuleapCredentialsSafe, AppError> {
let (tuleap_url, username, password) =
TuleapCredentials::validate_input(&tuleap_url, &username, &password)
.map_err(AppError::from)?;
let password_encrypted =
crypto::encrypt(&state.encryption_key, &password).map_err(AppError::from)?;

View file

@ -113,18 +113,150 @@ pub fn run() {
fn load_or_generate_key(path: &std::path::Path) -> Result<[u8; 32], Box<dyn std::error::Error>> {
use rand::RngCore;
if path.exists() {
let bytes = std::fs::read(path)?;
if bytes.len() != 32 {
return Err("Invalid key file size".into());
}
let mut key = [0u8; 32];
key.copy_from_slice(&bytes);
Ok(key)
} else {
let mut key = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut key);
std::fs::write(path, key)?;
Ok(key)
return read_key(path);
}
let mut key = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut key);
match write_new_key(path, &key) {
Ok(()) => Ok(key),
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => read_key(path),
Err(err) => Err(err.into()),
}
}
fn read_key(path: &std::path::Path) -> Result<[u8; 32], Box<dyn std::error::Error>> {
enforce_key_permissions(path)?;
let bytes = std::fs::read(path)?;
if bytes.len() != 32 {
return Err("Invalid key file size".into());
}
let mut key = [0u8; 32];
key.copy_from_slice(&bytes);
Ok(key)
}
fn write_new_key(path: &std::path::Path, key: &[u8; 32]) -> std::io::Result<()> {
#[cfg(unix)]
{
use std::fs::OpenOptions;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut file = OpenOptions::new()
.create_new(true)
.write(true)
.mode(0o600)
.open(path)?;
file.write_all(key)?;
file.sync_all()?;
}
#[cfg(not(unix))]
{
std::fs::write(path, key)?;
}
Ok(())
}
fn enforce_key_permissions(path: &std::path::Path) -> std::io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(path)?;
let current_mode = metadata.permissions().mode() & 0o777;
if current_mode != 0o600 {
let mut permissions = metadata.permissions();
permissions.set_mode(0o600);
std::fs::set_permissions(path, permissions)?;
}
}
#[cfg(not(unix))]
{
let _ = path;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_load_or_generate_key_creates_key() {
let dir = tempdir().expect("tempdir should be created");
let path = dir.path().join("orchai.key");
let key = load_or_generate_key(&path).expect("key generation should succeed");
assert_eq!(key.len(), 32);
let persisted = fs::read(&path).expect("key file should exist");
assert_eq!(persisted.len(), 32);
assert_eq!(persisted, key);
}
#[test]
fn test_load_or_generate_key_reads_existing_key() {
let dir = tempdir().expect("tempdir should be created");
let path = dir.path().join("orchai.key");
let expected = [42u8; 32];
fs::write(&path, expected).expect("existing key should be written");
let loaded = load_or_generate_key(&path).expect("existing key should be loaded");
assert_eq!(loaded, expected);
}
#[cfg(unix)]
#[test]
fn test_load_or_generate_key_creates_private_permissions() {
use std::os::unix::fs::PermissionsExt;
let dir = tempdir().expect("tempdir should be created");
let path = dir.path().join("orchai.key");
load_or_generate_key(&path).expect("key generation should succeed");
let mode = fs::metadata(&path)
.expect("metadata should be readable")
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o600);
}
#[cfg(unix)]
#[test]
fn test_load_or_generate_key_hardens_existing_permissions() {
use std::os::unix::fs::PermissionsExt;
let dir = tempdir().expect("tempdir should be created");
let path = dir.path().join("orchai.key");
fs::write(&path, [7u8; 32]).expect("existing key should be written");
let mut permissions = fs::metadata(&path)
.expect("metadata should be readable")
.permissions();
permissions.set_mode(0o644);
fs::set_permissions(&path, permissions).expect("permissions should be set");
load_or_generate_key(&path).expect("existing key should be loaded");
let mode = fs::metadata(&path)
.expect("metadata should be readable")
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o600);
}
}

View file

@ -18,6 +18,39 @@ pub struct TuleapCredentialsSafe {
}
impl TuleapCredentials {
pub fn validate_input(
tuleap_url: &str,
username: &str,
password: &str,
) -> std::result::Result<(String, String, String), String> {
let normalized_url = tuleap_url.trim().trim_end_matches('/').to_string();
if normalized_url.is_empty() {
return Err("Tuleap URL cannot be empty".to_string());
}
let parsed_url = reqwest::Url::parse(&normalized_url)
.map_err(|_| "Tuleap URL must be a valid absolute URL".to_string())?;
if !matches!(parsed_url.scheme(), "http" | "https") {
return Err("Tuleap URL must use http or https".to_string());
}
if parsed_url.host_str().is_none() {
return Err("Tuleap URL must include a host".to_string());
}
let normalized_username = username.trim().to_string();
if normalized_username.is_empty() {
return Err("Username cannot be empty".to_string());
}
if password.trim().is_empty() {
return Err("Password cannot be empty".to_string());
}
Ok((normalized_url, normalized_username, password.to_string()))
}
pub fn upsert(
conn: &Connection,
tuleap_url: &str,
@ -154,4 +187,48 @@ mod tests {
let result = TuleapCredentials::get(&conn).expect("get should succeed");
assert!(result.is_none());
}
#[test]
fn test_validate_input_trims_url_and_username() {
let (url, username, password) =
TuleapCredentials::validate_input(" https://tuleap.example.com/ ", " alice ", "secret")
.expect("validation should succeed");
assert_eq!(url, "https://tuleap.example.com");
assert_eq!(username, "alice");
assert_eq!(password, "secret");
}
#[test]
fn test_validate_input_rejects_empty_url() {
let result = TuleapCredentials::validate_input(" ", "alice", "secret");
assert!(result.is_err());
}
#[test]
fn test_validate_input_rejects_invalid_url() {
let result = TuleapCredentials::validate_input("not-an-url", "alice", "secret");
assert!(result.is_err());
}
#[test]
fn test_validate_input_rejects_non_http_scheme() {
let result =
TuleapCredentials::validate_input("ftp://tuleap.example.com", "alice", "secret");
assert!(result.is_err());
}
#[test]
fn test_validate_input_rejects_empty_username() {
let result =
TuleapCredentials::validate_input("https://tuleap.example.com", " ", "secret");
assert!(result.is_err());
}
#[test]
fn test_validate_input_rejects_empty_password() {
let result =
TuleapCredentials::validate_input("https://tuleap.example.com", "alice", " ");
assert!(result.is_err());
}
}