Skip to content

Commit

Permalink
Add optional encryption (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonwhite committed Apr 19, 2024
1 parent 30f76fe commit 224ee83
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 75 deletions.
13 changes: 9 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# Changelog

## v0.3.7

- Made encryption optional. If `--key` is not specified, LFS objects are not
encrypted.

## v0.3.6

- Bumped versions of various dependencies.
- Fixed Minio environment variables in `docker-compose.minio.yml`
* `MINIO_ACCESS_KEY` was renamed to `MINIO_ROOT_USER`
* `MINIO_SECRET_KEY` was renamed to `MINIO_ROOT_PASSWORD`
- Bumped versions of various dependencies.
- Fixed Minio environment variables in `docker-compose.minio.yml`
* `MINIO_ACCESS_KEY` was renamed to `MINIO_ROOT_USER`
* `MINIO_SECRET_KEY` was renamed to `MINIO_ROOT_PASSWORD`

## v0.3.5

Expand Down
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,25 @@ know by submitting an issue.

## Running It

### Generate an encryption key
### Generate an encryption key (optional)

All LFS objects are encrypted with the xchacha20 symmetric stream cipher. You
must generate a 32-byte encryption key before starting the server.
If configured, all LFS objects are encrypted with the xchacha20 symmetric stream
cipher. You must generate a 32-byte encryption key before starting the server.

Generating a random key is easy:

openssl rand -hex 32

Keep this secret and save it in a password manager so you don't lose it. We will
pass this to the server below.
pass this to the server below via the `--key` option. If the `--key` option is
**not** specified, then the LFS objects are **not** encrypted.

**Note**:
- If the key ever changes, all existing LFS objects will become garbage.
When the Git LFS client attempts to download them, the SHA256 verification
step will fail.
- If the key ever changes (or if encryption is disabled), all existing LFS
objects will become garbage. When the Git LFS client attempts to download
them, the SHA256 verification step will fail.
- Likewise, if encryption is later enabled after it has been disabled, all
existing unencrypted LFS objects will be seen as garbage.
- LFS objects in both the cache and in permanent storage are encrypted.
However, objects are decrypted before being sent to the LFS client, so take
any necessary precautions to keep your intellectual property safe.
Expand Down
3 changes: 0 additions & 3 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
max_width = 80
format_strings = true
error_on_line_overflow = true
error_on_unformatted = true
normalize_comments = true
wrap_comments = true
license_template_path = ".license_template"
64 changes: 45 additions & 19 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,19 @@ impl Cache {
#[derive(Debug)]
pub struct S3ServerBuilder {
bucket: String,
key: [u8; 32],
key: Option<[u8; 32]>,
prefix: Option<String>,
cdn: Option<String>,
cache: Option<Cache>,
}

impl S3ServerBuilder {
pub fn new(bucket: String, key: [u8; 32]) -> Self {
pub fn new(bucket: String) -> Self {
Self {
bucket,
prefix: None,
cdn: None,
key,
key: None,
cache: None,
}
}
Expand All @@ -107,7 +107,7 @@ impl S3ServerBuilder {

/// Sets the encryption key to use.
pub fn key(&mut self, key: [u8; 32]) -> &mut Self {
self.key = key;
self.key = Some(key);
self
}

Expand Down Expand Up @@ -174,13 +174,28 @@ impl S3ServerBuilder {
let disk = Faulty::new(disk);

let cache = Cached::new(cache.max_size, disk, s3).await?;
let storage = Verify::new(Encrypted::new(self.key, cache));
Ok(Box::new(spawn_server(storage, &addr)))
}
None => {
let storage = Verify::new(Encrypted::new(self.key, s3));
Ok(Box::new(spawn_server(storage, &addr)))

match self.key {
Some(key) => {
let storage = Verify::new(Encrypted::new(key, cache));
Ok(Box::new(spawn_server(storage, &addr)))
}
None => {
let storage = Verify::new(cache);
Ok(Box::new(spawn_server(storage, &addr)))
}
}
}
None => match self.key {
Some(key) => {
let storage = Verify::new(Encrypted::new(key, s3));
Ok(Box::new(spawn_server(storage, &addr)))
}
None => {
let storage = Verify::new(s3);
Ok(Box::new(spawn_server(storage, &addr)))
}
},
}
}

Expand All @@ -202,24 +217,24 @@ impl S3ServerBuilder {
#[derive(Debug)]
pub struct LocalServerBuilder {
path: PathBuf,
key: [u8; 32],
key: Option<[u8; 32]>,
cache: Option<Cache>,
}

impl LocalServerBuilder {
/// Creates a local server builder. `path` is the path to the folder where
/// all of the LFS data will be stored.
pub fn new(path: PathBuf, key: [u8; 32]) -> Self {
pub fn new(path: PathBuf) -> Self {
Self {
path,
key,
key: None,
cache: None,
}
}

/// Sets the encryption key to use.
pub fn key(&mut self, key: [u8; 32]) -> &mut Self {
self.key = key;
self.key = Some(key);
self
}

Expand All @@ -238,13 +253,24 @@ impl LocalServerBuilder {
pub async fn spawn(
self,
addr: SocketAddr,
) -> Result<impl Server, Box<dyn std::error::Error>> {
) -> Result<Box<dyn Server + Unpin + Send>, Box<dyn std::error::Error>>
{
let storage = Disk::new(self.path).map_err(Error::from).await?;
let storage = Verify::new(Encrypted::new(self.key, storage));

log::info!("Local disk storage initialized.");

Ok(spawn_server(storage, &addr))
match self.key {
Some(key) => {
let storage = Verify::new(Encrypted::new(key, storage));
log::info!("Local disk storage initialized (with encryption).");
Ok(Box::new(spawn_server(storage, &addr)))
}
None => {
let storage = Verify::new(storage);
log::info!(
"Local disk storage initialized (without encryption)."
);
Ok(Box::new(spawn_server(storage, &addr)))
}
}
}

/// Spawns the server and runs it to completion. This will run forever
Expand Down
14 changes: 11 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ struct GlobalArgs {
parse(try_from_str = FromHex::from_hex),
env = "RUDOLFS_KEY"
)]
key: [u8; 32],
key: Option<[u8; 32]>,

/// Root directory of the object cache. If not specified or if the local
/// disk is the storage backend, then no local disk cache will be used.
Expand Down Expand Up @@ -156,9 +156,13 @@ impl S3Args {
addr: SocketAddr,
global_args: GlobalArgs,
) -> Result<(), Box<dyn std::error::Error>> {
let mut builder = S3ServerBuilder::new(self.bucket, global_args.key);
let mut builder = S3ServerBuilder::new(self.bucket);
builder.prefix(self.prefix);

if let Some(key) = global_args.key {
builder.key(key);
}

if let Some(cdn) = self.cdn {
builder.cdn(cdn);
}
Expand All @@ -181,7 +185,11 @@ impl LocalArgs {
addr: SocketAddr,
global_args: GlobalArgs,
) -> Result<(), Box<dyn std::error::Error>> {
let mut builder = LocalServerBuilder::new(self.path, global_args.key);
let mut builder = LocalServerBuilder::new(self.path);

if let Some(key) = global_args.key {
builder.key(key);
}

if let Some(cache_dir) = global_args.cache_dir {
let max_cache_size = global_args
Expand Down
1 change: 1 addition & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#![allow(unused)]
use std::fs::{self, File};
use std::io;
use std::net::SocketAddr;
Expand Down
73 changes: 57 additions & 16 deletions tests/test_local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,22 @@
// SOFTWARE.
mod common;

use std::io;
use std::net::SocketAddr;
use std::path::Path;

use futures::future::Either;
use rand::rngs::StdRng;
use rand::Rng;
use rand::SeedableRng;
use rudolfs::{LocalServerBuilder, Server};
use rudolfs::LocalServerBuilder;
use tokio::sync::oneshot;

use common::{init_logger, GitRepo};

#[tokio::test(flavor = "multi_thread")]
async fn local_smoke_test() -> Result<(), Box<dyn std::error::Error>> {
async fn local_smoke_test_encrypted() -> Result<(), Box<dyn std::error::Error>>
{
init_logger();

// Make sure our seed is deterministic. This makes it easier to reproduce
Expand All @@ -42,18 +44,64 @@ async fn local_smoke_test() -> Result<(), Box<dyn std::error::Error>> {
let data = tempfile::TempDir::new()?;
let key = rng.gen();

let server = LocalServerBuilder::new(data.path().into(), key);
let mut server = LocalServerBuilder::new(data.path().into());
server.key(key);
let server = server.spawn(SocketAddr::from(([0, 0, 0, 0], 0))).await?;
let addr = server.addr();

let (shutdown_tx, shutdown_rx) = oneshot::channel();

let server = tokio::spawn(futures::future::select(shutdown_rx, server));

exercise_server(addr, &mut rng)?;

shutdown_tx.send(()).expect("server died too soon");

if let Either::Right((result, _)) = server.await? {
// If the server exited first, then propagate the error.
result?;
}

Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn local_smoke_test_unencrypted() -> Result<(), Box<dyn std::error::Error>>
{
init_logger();

// Make sure our seed is deterministic. This makes it easier to reproduce
// the same repo every time.
let mut rng = StdRng::seed_from_u64(42);

let data = tempfile::TempDir::new()?;

let server = LocalServerBuilder::new(data.path().into());
let server = server.spawn(SocketAddr::from(([0, 0, 0, 0], 0))).await?;
let addr = server.addr();

let (shutdown_tx, shutdown_rx) = oneshot::channel();

let server = tokio::spawn(futures::future::select(shutdown_rx, server));

exercise_server(addr, &mut rng)?;

shutdown_tx.send(()).expect("server died too soon");

if let Either::Right((result, _)) = server.await? {
// If the server exited first, then propagate the error.
result?;
}

Ok(())
}

/// Creates a repository with a few LFS files in it to exercise the LFS server.
fn exercise_server(addr: SocketAddr, rng: &mut impl Rng) -> io::Result<()> {
let repo = GitRepo::init(addr)?;
repo.add_random(Path::new("4mb.bin"), 4 * 1024 * 1024, &mut rng)?;
repo.add_random(Path::new("8mb.bin"), 8 * 1024 * 1024, &mut rng)?;
repo.add_random(Path::new("16mb.bin"), 16 * 1024 * 1024, &mut rng)?;
repo.add_random(Path::new("4mb.bin"), 4 * 1024 * 1024, rng)?;
repo.add_random(Path::new("8mb.bin"), 8 * 1024 * 1024, rng)?;
repo.add_random(Path::new("16mb.bin"), 16 * 1024 * 1024, rng)?;
repo.commit("Add LFS objects")?;

// Make sure we can push LFS objects to the server.
Expand All @@ -73,19 +121,12 @@ async fn local_smoke_test() -> Result<(), Box<dyn std::error::Error>> {
repo_clone.lfs_pull()?;

// Add some more files and make sure you can pull those into the clone
repo.add_random(Path::new("4mb_2.bin"), 4 * 1024 * 1024, &mut rng)?;
repo.add_random(Path::new("8mb_2.bin"), 8 * 1024 * 1024, &mut rng)?;
repo.add_random(Path::new("16mb_2.bin"), 16 * 1024 * 1024, &mut rng)?;
repo.add_random(Path::new("4mb_2.bin"), 4 * 1024 * 1024, rng)?;
repo.add_random(Path::new("8mb_2.bin"), 8 * 1024 * 1024, rng)?;
repo.add_random(Path::new("16mb_2.bin"), 16 * 1024 * 1024, rng)?;
repo.commit("Add LFS objects 2")?;

repo_clone.pull()?;

shutdown_tx.send(()).expect("server died too soon");

if let Either::Right((result, _)) = server.await? {
// If the server exited first, then propagate the error.
result?;
}

Ok(())
}

0 comments on commit 224ee83

Please sign in to comment.