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

[server] Dynamically loaded pepper #1421 #1450

Merged
merged 2 commits into from
Jan 3, 2025
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
107 changes: 105 additions & 2 deletions agdb_server/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::password::SALT_LEN;
use crate::server_error::ServerError;
use crate::server_error::ServerResult;
use serde::Deserialize;
Expand All @@ -21,12 +22,15 @@ pub(crate) struct ConfigImpl {
pub(crate) admin: String,
pub(crate) log_level: LogLevel,
pub(crate) data_dir: String,
pub(crate) pepper_path: String,
pub(crate) cluster_token: String,
pub(crate) cluster: Vec<Url>,
#[serde(skip)]
pub(crate) cluster_node_id: usize,
#[serde(skip)]
pub(crate) start_time: u64,
#[serde(skip)]
pub(crate) pepper: Option<[u8; SALT_LEN]>,
}

pub(crate) fn new(config_file: &str) -> ServerResult<Config> {
Expand All @@ -38,11 +42,29 @@ pub(crate) fn new(config_file: &str) -> ServerResult<Config> {
.position(|x| x == &config_impl.address)
.unwrap_or(0);
config_impl.start_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();

if !config_impl.pepper_path.is_empty() {
let pepper = std::fs::read(&config_impl.pepper_path)?;

if pepper.len() != SALT_LEN {
return Err(ServerError::from(format!(
"invalid pepper length {}, expected 16",
pepper.len()
)));
}

config_impl.pepper = Some(
pepper[0..SALT_LEN]
.try_into()
.expect("pepper length should be 16"),
);
}

let config = Config::new(config_impl);

if !config.cluster.is_empty() && !config.cluster.contains(&config.address) {
return Err(ServerError::from(format!(
"Cluster does not contain local node: {}",
"cluster does not contain local node: {}",
config.address
)));
}
Expand All @@ -57,10 +79,12 @@ pub(crate) fn new(config_file: &str) -> ServerResult<Config> {
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
data_dir: "agdb_server_data".to_string(),
pepper_path: String::new(),
cluster_token: "cluster".to_string(),
cluster: vec![],
cluster_node_id: 0,
start_time: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
pepper: None,
};

std::fs::write(config_file, serde_yml::to_string(&config)?)?;
Expand Down Expand Up @@ -143,15 +167,94 @@ mod tests {
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
data_dir: "agdb_server_data".to_string(),
pepper_path: String::new(),
cluster_token: "cluster".to_string(),
cluster: vec![Url::parse("localhost:3001").unwrap()],
cluster_node_id: 0,
start_time: 0,
pepper: None,
};
std::fs::write(test_file.filename, serde_yml::to_string(&config).unwrap()).unwrap();
assert_eq!(
config::new(test_file.filename).unwrap_err().description,
"cluster does not contain local node: localhost:3000"
);
}

#[test]
fn pepper_path() {
let test_file = TestFile::new("pepper_path.yaml");
let pepper_file = TestFile::new("pepper_path");
let pepper = b"abcdefghijklmnop";
std::fs::write(pepper_file.filename, pepper).unwrap();
let config = ConfigImpl {
bind: ":::3000".to_string(),
address: Url::parse("localhost:3000").unwrap(),
basepath: "".to_string(),
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
data_dir: "agdb_server_data".to_string(),
pepper_path: pepper_file.filename.to_string(),
cluster_token: "cluster".to_string(),
cluster: vec![],
cluster_node_id: 0,
start_time: 0,
pepper: None,
};

std::fs::write(test_file.filename, serde_yml::to_string(&config).unwrap()).unwrap();

let config = config::new(test_file.filename).unwrap();

assert_eq!(config.pepper.as_ref(), Some(pepper));
}

#[test]
fn pepper_missing() {
let test_file = TestFile::new("pepper_missing.yaml");
let config = ConfigImpl {
bind: ":::3000".to_string(),
address: Url::parse("localhost:3000").unwrap(),
basepath: "".to_string(),
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
data_dir: "agdb_server_data".to_string(),
pepper_path: "missing_file".to_string(),
cluster_token: "cluster".to_string(),
cluster: vec![],
cluster_node_id: 0,
start_time: 0,
pepper: None,
};
std::fs::write(test_file.filename, serde_yml::to_string(&config).unwrap()).unwrap();

assert!(config::new(test_file.filename).is_err());
}

#[test]
fn pepper_invalid_len() {
let test_file = TestFile::new("pepper_invalid_len.yaml");
let pepper_file = TestFile::new("pepper_invalid_len");
std::fs::write(pepper_file.filename, b"0123456789").unwrap();
let config = ConfigImpl {
bind: ":::3000".to_string(),
address: Url::parse("localhost:3000").unwrap(),
basepath: "".to_string(),
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
data_dir: "agdb_server_data".to_string(),
pepper_path: pepper_file.filename.to_string(),
cluster_token: "cluster".to_string(),
cluster: vec![],
cluster_node_id: 0,
start_time: 0,
pepper: None,
};
std::fs::write(test_file.filename, serde_yml::to_string(&config).unwrap()).unwrap();

assert_eq!(
config::new(test_file.filename).unwrap_err().description,
"Cluster does not contain local node: localhost:3000"
"invalid pepper length 10, expected 16"
);
}

Expand Down
2 changes: 2 additions & 0 deletions agdb_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ async fn main() -> ServerResult {
.with_max_level(config.log_level.0)
.init();

password::init(config.pepper);

let (shutdown_sender, shutdown_receiver) = broadcast::channel::<()>(1);
let server_db = server_db::new(&config).await?;
let db_pool = db_pool::new(config.clone(), &server_db).await?;
Expand Down
23 changes: 18 additions & 5 deletions agdb_server/src/password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ use ring::rand::SystemRandom;
use serde::Deserialize;
use serde::Serialize;
use std::num::NonZeroU32;
use std::sync::OnceLock;

pub(crate) const PASSWORD_LEN: usize = digest::SHA256_OUTPUT_LEN;
pub(crate) const SALT_LEN: usize = 16;

pub(crate) static BUILTIN_PEPPER: &[u8; SALT_LEN] = std::include_bytes!("../pepper");
pub(crate) static PEPPER: OnceLock<[u8; SALT_LEN]> = OnceLock::new();
static ALGORITHM: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;
static PEPPER: &[u8; SALT_LEN] = std::include_bytes!("../pepper");
static DB_SALT: [u8; SALT_LEN] = [
198, 78, 119, 143, 114, 32, 22, 184, 167, 93, 196, 63, 154, 18, 14, 79,
];
Expand Down Expand Up @@ -74,19 +77,27 @@ impl Password {
}

fn salt(user: &str, user_salt: [u8; SALT_LEN]) -> Vec<u8> {
let mut salt = Vec::with_capacity(
user.as_bytes().len() + user_salt.len() + DB_SALT.len() + PEPPER.len(),
);
let mut salt = Vec::with_capacity(user.as_bytes().len() + SALT_LEN * 3);

salt.extend(DB_SALT);
salt.extend(user.as_bytes());
salt.extend(PEPPER);
salt.extend(PEPPER.get().expect("pepper should be initialized"));
salt.extend(user_salt);

salt
}
}

pub(crate) fn init(pepper: Option<[u8; SALT_LEN]>) {
PEPPER.get_or_init(|| {
if let Some(p) = pepper {
p
} else {
*BUILTIN_PEPPER
}
});
}

pub(crate) fn validate_password(password: &str) -> ServerResult {
if password.len() < 8 {
Err(ErrorCode::PasswordTooShort.into())
Expand All @@ -109,6 +120,8 @@ mod tests {

#[test]
fn verify_password() -> ServerResult {
init(None);

let password = Password::create("alice", "MyPassword123");
assert_ne!(password.password, [0_u8; PASSWORD_LEN]);
assert_ne!(password.user_salt, [0_u8; SALT_LEN]);
Expand Down
1 change: 1 addition & 0 deletions agdb_server/tests/routes/misc_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ async fn basepath_test() -> anyhow::Result<()> {
config.insert("data_dir", SERVER_DATA_DIR.into());
config.insert("basepath", "/public".into());
config.insert("log_level", "INFO".into());
config.insert("pepper_path", "".into());
config.insert("cluster_token", "test".into());
config.insert("cluster", Vec::<String>::new().into());

Expand Down
2 changes: 2 additions & 0 deletions agdb_server/tests/test_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ impl TestServerImpl {
config.insert("data_dir", SERVER_DATA_DIR.into());
config.insert("basepath", "".into());
config.insert("log_level", "INFO".into());
config.insert("pepper_path", "".into());
config.insert("cluster_token", "test".into());
config.insert("cluster", Vec::<String>::new().into());

Expand Down Expand Up @@ -376,6 +377,7 @@ pub async fn create_cluster(nodes: usize) -> anyhow::Result<Vec<TestServerImpl>>
config.insert("basepath", "".into());
config.insert("log_level", "INFO".into());
config.insert("data_dir", SERVER_DATA_DIR.into());
config.insert("pepper_path", "".into());
config.insert("cluster_token", "test".into());

configs.push(config);
Expand Down
6 changes: 5 additions & 1 deletion agdb_web/pages/en-US/docs/guides/how-to-run-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ For non-production use:
cargo install agdb_server
```

For production a manual build & install is recommended. You would build `agdb_server` from source in release mode with a custom `pepper` file. The `pepper` file located in sources as `agdb_server/pepper` contains a random 16 character value that is used internally to additionally "season" the encrypted passwords. When building for production you should change this value to a different one and keep the pepper file as secret in case you needed to rebuild the server or build a new version.
For production use there are several options:

1. Manual build & install. You would build `agdb_server` from source in release mode with a custom `pepper` file. The `pepper` file located in sources as `agdb_server/pepper` contains a random 16 character value that is used internally to additionally "season" the encrypted passwords. When building for production you should change this value to a different one and keep the pepper file as secret in case you needed to rebuild the server or build a new version.

The steps for a production/manual build (use `bash` on Unix or `git bash` on Windows):

Expand All @@ -51,6 +53,8 @@ mv target/release/agdb_server "<location available on your PATH>"
(including the old admin account).
</Callout>

2. Use default pepper value (it is still recommended to build manually and use a different value) but specify in configuration the "pepper_path" from which the pepper would be loaded in runtime. This file and location should be treated as secret and all of the caveats from step 1 still apply. On the other hand you would not need to rebuild the server in the future.

### Run the server

```bash
Expand Down
1 change: 1 addition & 0 deletions agdb_web/pages/en-US/docs/references/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ basepath: "" # base path to append to the address in case the server is to be ru
admin: admin # the admin user that will be created automatically for the server, the password will be the same as name (admin by default, recommended to change after startup)
data_dir: agdb_server_data # directory to store user data
log_level: INFO # Options are: OFF, ERROR, WARN, INFO, DEBUG, TRACE
pepper_path: "" # Optional path to a runtime secret file containing 16 bytes "pepper" value for additionally "seasoning" (hashing) passwords. If empty a built-in pepper value is used - see "How to run the server?"" guide for details
```

You can prepare it in advance in a file `agdb_server.yaml`. After the server database is created changes to the `admin` field will have no effect, but the other settings can be changed later. All config changes require server restart to take effect.
Expand Down
Loading