Something went wrong. Try again.
Game sync and live services for independent game developers (targeting itch.io)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129use age::Encryptor;use age::IdentityFile;use miette::Diagnostic;use rand::RngCore;use std::io::Write;use std::iter;use std::os::unix::fs::PermissionsExt;use std::path::Path;use thiserror::Error;
#[derive(Error, Debug, Diagnostic)]pub enum Error { #[error("key file not found at path")] #[diagnostic(code(scratchback::key::not_found))] KeyNotFound { path: String, help: Option<String> },
#[error("key file has insecure permissions: {path} ({mode:o})")] #[diagnostic(code(scratchback::key::insecure_permissions))] InsecureKeyPermissions { path: String, mode: u32, help: Option<String>, },
#[error("invalid age secret key format")] #[diagnostic(code(scratchback::key::invalid_format))] #[help("ensure key is in bech32 format (age1...)")] InvalidKeyFormat,
#[error("encryption failed")] #[diagnostic(code(scratchback::crypto::encryption_failed))] EncryptionFailed,
#[error("database error: {0}")] #[diagnostic(code(scratchback::crypto::database_error))] Database(String),
#[error("invalid request: {0}")] #[diagnostic(code(scratchback::crypto::invalid_request))] InvalidRequest(String),}
struct SyncRecipient(Box<dyn age::Recipient + Send>);
unsafe impl Sync for SyncRecipient {}
pub struct MasterKey { recipient: SyncRecipient,}
impl MasterKey { pub fn from_identity(identity: age::x25519::Identity) -> Self { Self { recipient: SyncRecipient(Box::new(identity.to_public())), } }
pub fn load_from_file(path: &Path) -> Result<Self, Error> { if !path.exists() { return Err(Error::KeyNotFound { path: path.display().to_string(), help: Some("set SCRATCHBACK_AGE_KEY_PATH to a valid key file path".into()), }); }
let metadata = std::fs::metadata(path).map_err(|_| Error::KeyNotFound { path: path.display().to_string(), help: None, })?;
let mode = metadata.permissions().mode() & 0o777; if mode != 0o600 { return Err(Error::InsecureKeyPermissions { path: path.display().to_string(), mode, help: Some("key file must be mode 0600".into()), }); }
let identity_file = IdentityFile::from_file(path.to_string_lossy().to_string()) .map_err(|_| Error::InvalidKeyFormat)?;
let mut recipients = identity_file .to_recipients() .map_err(|_| Error::InvalidKeyFormat)?;
let recipient = recipients.pop().ok_or(Error::InvalidKeyFormat)?;
Ok(Self { recipient: SyncRecipient(recipient), }) }
pub fn encrypt_dek(&self, dek: &[u8]) -> Result<Vec<u8>, Error> { let encryptor = Encryptor::with_recipients(iter::once(&*self.recipient.0 as &dyn age::Recipient)) .map_err(|_| Error::EncryptionFailed)?;
let mut encrypted = Vec::new(); let mut writer = encryptor .wrap_output(&mut encrypted) .map_err(|_| Error::EncryptionFailed)?;
writer.write_all(dek).map_err(|_| Error::EncryptionFailed)?; writer.finish().map_err(|_| Error::EncryptionFailed)?;
Ok(encrypted) }}
pub fn load_master_key() -> Result<MasterKey, Error> { let key_path = std::env::var("SCRATCHBACK_AGE_KEY_PATH").map_err(|_| Error::KeyNotFound { path: "(not set)".to_string(), help: Some("set SCRATCHBACK_AGE_KEY_PATH environment variable".into()), })?;
MasterKey::load_from_file(Path::new(&key_path))}
pub fn generate_dek() -> [u8; 32] { let mut dek = [0u8; 32]; rand::thread_rng().fill_bytes(&mut dek); dek}
pub fn encrypt_dek(master_key: &MasterKey, dek: &[u8]) -> Result<Vec<u8>, Error> { master_key.encrypt_dek(dek)}