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, nonce: Vec, } #[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 { 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); } }