diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eceb22e4..21296a61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,8 +26,12 @@ jobs: run: cargo fmt --check - name: Clippy run: cargo clippy -- -D warnings + env: + FERRFLOW_HMAC_SECRET: ${{ secrets.FERRFLOW_HMAC_SECRET }} - name: Tests run: cargo test + env: + FERRFLOW_HMAC_SECRET: ${{ secrets.FERRFLOW_HMAC_SECRET }} coverage: name: Coverage @@ -40,6 +44,8 @@ jobs: run: cargo install cargo-tarpaulin - name: Generate coverage run: cargo tarpaulin --out xml --skip-clean + env: + FERRFLOW_HMAC_SECRET: ${{ secrets.FERRFLOW_HMAC_SECRET }} - uses: codecov/codecov-action@v6 with: files: cobertura.xml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c3f867f..1c8dadf1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,9 +59,13 @@ jobs: - name: Build (Linux) if: matrix.os == 'ubuntu-latest' run: cross build --release --target ${{ matrix.target }} + env: + FERRFLOW_HMAC_SECRET: ${{ secrets.FERRFLOW_HMAC_SECRET }} - name: Build (macOS / Windows) if: matrix.os != 'ubuntu-latest' run: cargo build --release --target ${{ matrix.target }} + env: + FERRFLOW_HMAC_SECRET: ${{ secrets.FERRFLOW_HMAC_SECRET }} - name: Package (Unix) if: matrix.os != 'windows-latest' run: | diff --git a/Cargo.lock b/Cargo.lock index 24db798f..697f0986 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,6 +247,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmov" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3" + [[package]] name = "colorchoice" version = "1.0.5" @@ -396,6 +402,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctutils" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1005a6d4446f5120ef475ad3d2af2b30c49c2c9c6904258e3bb30219bebed5e4" +dependencies = [ + "cmov", +] + [[package]] name = "deranged" version = "0.5.8" @@ -414,6 +429,7 @@ dependencies = [ "block-buffer", "const-oid", "crypto-common", + "ctutils", ] [[package]] @@ -476,6 +492,7 @@ dependencies = [ "criterion", "git2", "hex", + "hmac", "json5", "regex", "semver", @@ -617,6 +634,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.4.0" diff --git a/Cargo.toml b/Cargo.toml index 239c9290..c0903e5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ path = "src/lib.rs" [features] default = ["cli"] -cli = ["dep:git2", "dep:ureq", "dep:clap", "dep:colored"] +cli = ["dep:git2", "dep:ureq", "dep:clap", "dep:colored", "dep:hmac"] [dependencies] serde = { version = "1", features = ["derive"] } @@ -47,6 +47,7 @@ git2 = { version = "0.20", features = ["vendored-libgit2", "vendored-openssl"], ureq = { version = "3", features = ["json"], optional = true } sha2 = "0.11.0" hex = "0.4.3" +hmac = { version = "0.13", optional = true } [dev-dependencies] cargo-husky = { version = "1", default-features = false, features = ["user-hooks"] } diff --git a/src/telemetry.rs b/src/telemetry.rs index 6895f726..fd564c37 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -1,8 +1,16 @@ +use hmac::{Hmac, KeyInit, Mac}; use serde::Serialize; use sha2::Digest; +use std::time::{SystemTime, UNIX_EPOCH}; + +type HmacSha256 = Hmac; const DEFAULT_API_URL: &str = "https://api.ferrflow.com"; +fn hmac_secret() -> &'static str { + option_env!("FERRFLOW_HMAC_SECRET").unwrap_or(env!("FERRFLOW_HMAC_SECRET")) +} + #[derive(Debug, Clone, Copy, Serialize)] #[serde(rename_all = "snake_case")] #[allow(dead_code)] @@ -83,11 +91,30 @@ pub fn send_event( let url = format!("{}/events", api_url()); std::thread::spawn(move || { + let body = match serde_json::to_string(&payload) { + Ok(b) => b, + Err(_) => return, + }; + + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + .to_string(); + + let message = format!("{timestamp}.{body}"); + let mut mac = HmacSha256::new_from_slice(hmac_secret().as_bytes()) + .expect("HMAC accepts any key length"); + mac.update(message.as_bytes()); + let signature = hex::encode(mac.finalize().into_bytes()); + let agent = ureq::Agent::new_with_defaults(); let _ = agent .post(&url) .header("Content-Type", "application/json") - .send_json(&payload); + .header("X-Timestamp", ×tamp) + .header("X-Signature", &signature) + .send(body.as_bytes()); }); }