198 lines
5.9 KiB
Rust
198 lines
5.9 KiB
Rust
use chacha20poly1305::XNonce;
|
|
use serde::{Serialize, Deserialize};
|
|
use sqlx::{
|
|
FromRow,
|
|
Sqlite,
|
|
Transaction,
|
|
types::Uuid,
|
|
};
|
|
|
|
use super::{Credential, Crypto, PersistentCredential};
|
|
|
|
use crate::errors::*;
|
|
|
|
|
|
#[derive(Debug, Clone, FromRow)]
|
|
pub struct DockerRow {
|
|
id: Uuid,
|
|
server_url: String,
|
|
username: String,
|
|
secret_enc: Vec<u8>,
|
|
nonce: Vec<u8>,
|
|
}
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub struct DockerCredential {
|
|
#[serde(rename = "ServerURL")]
|
|
pub server_url: String,
|
|
pub username: String,
|
|
pub secret: String,
|
|
}
|
|
|
|
|
|
impl PersistentCredential for DockerCredential {
|
|
type Row = DockerRow;
|
|
|
|
fn type_name() -> &'static str { "docker" }
|
|
|
|
fn into_credential(self) -> Credential { Credential::Docker(self) }
|
|
|
|
fn row_id(row: &DockerRow) -> Uuid { row.id }
|
|
|
|
fn from_row(row: DockerRow, crypto: &Crypto) -> Result<Self, LoadCredentialsError> {
|
|
let nonce = XNonce::clone_from_slice(&row.nonce);
|
|
let secret_bytes = crypto.decrypt(&nonce, &row.secret_enc)?;
|
|
let secret = String::from_utf8(secret_bytes)
|
|
.map_err(|_| LoadCredentialsError::InvalidData)?;
|
|
|
|
Ok(DockerCredential {
|
|
server_url: row.server_url,
|
|
username: row.username,
|
|
secret
|
|
})
|
|
}
|
|
|
|
async fn save_details(&self, id: &Uuid, crypto: &Crypto, txn: &mut Transaction<'_, Sqlite>) -> Result<(), SaveCredentialsError> {
|
|
let (nonce, ciphertext) = crypto.encrypt(self.secret.as_bytes())?;
|
|
let nonce_bytes = &nonce.as_slice();
|
|
|
|
sqlx::query!(
|
|
"INSERT OR REPLACE INTO docker_credentials (
|
|
id,
|
|
server_url,
|
|
username,
|
|
secret_enc,
|
|
nonce
|
|
)
|
|
VALUES (?, ?, ?, ?, ?)",
|
|
id, self.server_url, self.username, ciphertext, nonce_bytes,
|
|
).execute(&mut **txn).await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::credentials::CredentialRecord;
|
|
use creddy_cli::proto::DockerCredential as CliDockerCredential;
|
|
use sqlx::SqlitePool;
|
|
use sqlx::types::uuid::uuid;
|
|
|
|
|
|
|
|
fn test_credential() -> DockerCredential {
|
|
DockerCredential {
|
|
server_url: "https://registry.jfmonty2.com".into(),
|
|
username: "joe@jfmonty2.com".into(),
|
|
secret: "correct horse battery staple".into(),
|
|
}
|
|
}
|
|
|
|
fn test_credential_2() -> DockerCredential {
|
|
DockerCredential {
|
|
server_url: "https://index.docker.io/v1".into(),
|
|
username: "test@example.com".into(),
|
|
secret: "a very secure passphrase".into(),
|
|
}
|
|
}
|
|
|
|
fn test_record() -> CredentialRecord {
|
|
CredentialRecord {
|
|
id: uuid!("00000000-0000-0000-0000-000000000000"),
|
|
name: "docker_test".into(),
|
|
is_default: false,
|
|
credential: Credential::Docker(test_credential()),
|
|
}
|
|
}
|
|
|
|
fn test_record_2() -> CredentialRecord {
|
|
CredentialRecord {
|
|
id: uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff"),
|
|
name: "docker_test_2".into(),
|
|
is_default: false,
|
|
credential: Credential::Docker(test_credential_2()),
|
|
}
|
|
}
|
|
|
|
|
|
#[sqlx::test]
|
|
fn test_save(pool: SqlitePool) {
|
|
let crypt = Crypto::random();
|
|
test_record().save(&crypt, &pool).await
|
|
.expect("Failed to save record");
|
|
}
|
|
|
|
#[sqlx::test(fixtures("docker_credentials"))]
|
|
fn test_load(pool: SqlitePool) {
|
|
let crypt = Crypto::fixed();
|
|
let id = uuid!("00000000-0000-0000-0000-000000000000");
|
|
let loaded = DockerCredential::load(&id, &crypt, &pool).await
|
|
.expect("Failed to load record");
|
|
|
|
assert_eq!(test_credential(), loaded);
|
|
}
|
|
|
|
#[sqlx::test(fixtures("docker_credentials"))]
|
|
async fn test_overwrite(pool: SqlitePool) {
|
|
let crypt = Crypto::fixed();
|
|
let mut record = test_record_2();
|
|
// give it the same id as test_record so that it overwrites
|
|
let id = uuid!("00000000-0000-0000-0000-000000000000");
|
|
record.id = id;
|
|
record.save(&crypt, &pool).await
|
|
.expect("Failed to overwrite original record with second record");
|
|
|
|
let loaded = DockerCredential::load(&id, &crypt, &pool).await
|
|
.expect("Failed to load again after overwriting");
|
|
|
|
assert_eq!(test_credential_2(), loaded);
|
|
}
|
|
|
|
#[sqlx::test(fixtures("docker_credentials"))]
|
|
async fn test_list(pool: SqlitePool) {
|
|
let crypt = Crypto::fixed();
|
|
let records = CredentialRecord::list(&crypt, &pool).await
|
|
.expect("Failed to list credentials");
|
|
|
|
assert_eq!(test_record(), records[0]);
|
|
}
|
|
|
|
|
|
// make sure that CLI credentials and app credentials don't drift apart
|
|
#[test]
|
|
fn test_cli_to_app() {
|
|
let cli_creds = CliDockerCredential {
|
|
server_url: "https://registry.jfmonty2.com".into(),
|
|
username: "joe@jfmonty2.com".into(),
|
|
secret: "correct horse battery staple".into(),
|
|
};
|
|
|
|
let json = serde_json::to_string(&cli_creds).unwrap();
|
|
let computed: DockerCredential = serde_json::from_str(&json)
|
|
.expect("Failed to deserialize Docker credentials from CLI -> main app");
|
|
|
|
assert_eq!(test_credential(), computed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_app_to_cli() {
|
|
let app_creds = test_credential();
|
|
let json = serde_json::to_string(&app_creds).unwrap();
|
|
|
|
let computed: CliDockerCredential = serde_json::from_str(&json)
|
|
.expect("Failed to deserialize Docker credentials from main app -> CLI");
|
|
|
|
let expected = CliDockerCredential {
|
|
server_url: "https://registry.jfmonty2.com".into(),
|
|
username: "joe@jfmonty2.com".into(),
|
|
secret: "correct horse battery staple".into(),
|
|
};
|
|
assert_eq!(expected, computed);
|
|
}
|
|
}
|