use std::fmt::Write as _;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
use sha2::{Digest, Sha256};
use crate::error::{Error, Result};
const CHUNK: usize = 64 * 1024;
pub fn sha256_hex(path: &Path) -> Result<String> {
let mut file = File::open(path)?;
let mut hasher = Sha256::new();
let mut buf = vec![0u8; CHUNK];
loop {
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
let mut out = String::with_capacity(64);
for byte in hasher.finalize() {
let _ = write!(out, "{byte:02x}");
}
Ok(out)
}
pub fn verify_sha256(path: &Path, expected_hex: &str) -> Result<()> {
let actual = sha256_hex(path)?;
if actual.eq_ignore_ascii_case(expected_hex.trim()) {
Ok(())
} else {
Err(Error::ChecksumMismatch {
expected: expected_hex.trim().to_ascii_lowercase(),
actual,
})
}
}
pub fn verify_minisign(path: &Path, public_key: &str, signature: &str) -> Result<()> {
let pk = minisign_verify::PublicKey::from_base64(public_key.trim())
.map_err(|e| Error::Signature(format!("invalid public key: {e}")))?;
let sig = minisign_verify::Signature::decode(signature)
.map_err(|e| Error::Signature(format!("invalid signature: {e}")))?;
let data = std::fs::read(path)?;
pk.verify(&data, &sig, false)
.map_err(|e| Error::Signature(e.to_string()))
}
pub fn verify_ed25519(path: &Path, public_key: &str, signature: &str) -> Result<()> {
let pk = decode_base64("public key", public_key).and_then(|bytes| {
ed25519_compact::PublicKey::from_slice(&bytes)
.map_err(|e| Error::Signature(format!("invalid Ed25519 public key: {e}")))
})?;
let sig = decode_base64("signature", signature).and_then(|bytes| {
ed25519_compact::Signature::from_slice(&bytes)
.map_err(|e| Error::Signature(format!("invalid Ed25519 signature: {e}")))
})?;
let mut state = pk
.verify_incremental(&sig)
.map_err(|e| Error::Signature(format!("invalid Ed25519 signature: {e}")))?;
let mut file = File::open(path)?;
let mut buf = vec![0u8; CHUNK];
loop {
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
state.absorb(&buf[..n]);
}
state
.verify()
.map_err(|_| Error::Signature("Ed25519 signature does not match the file".into()))
}
fn decode_base64(what: &str, value: &str) -> Result<Vec<u8>> {
BASE64
.decode(value.trim())
.map_err(|e| Error::Signature(format!("Ed25519 {what} is not valid base64: {e}")))
}
#[cfg(test)]
pub(crate) mod test_support {
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::BASE64;
use base64::Engine as _;
pub(crate) struct Ed25519Fixture {
pub(crate) path: PathBuf,
pub(crate) public_key: String,
pub(crate) signature: String,
}
impl Ed25519Fixture {
pub(crate) fn new(contents: &[u8]) -> Self {
let seed = ed25519_compact::Seed::new([7; ed25519_compact::Seed::BYTES]);
let pair = ed25519_compact::KeyPair::from_seed(seed);
let signature = pair.sk.sign(contents, None);
let path = unique_temp_path("ed25519");
std::fs::write(&path, contents).unwrap();
Self {
path,
public_key: BASE64.encode(*pair.pk),
signature: BASE64.encode(*signature),
}
}
}
impl Drop for Ed25519Fixture {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
pub(crate) fn unique_temp_path(tag: &str) -> PathBuf {
static NEXT: AtomicUsize = AtomicUsize::new(0);
let n = NEXT.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"gpui-updater-test-{}-{tag}-{n}.bin",
std::process::id()
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn temp_file(contents: &[u8]) -> std::path::PathBuf {
let path = test_support::unique_temp_path("sha256");
let mut f = File::create(&path).unwrap();
f.write_all(contents).unwrap();
path
}
#[test]
fn sha256_matches_known_vector() {
let path = temp_file(b"abc");
let got = sha256_hex(&path).unwrap();
assert_eq!(
got,
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
assert!(verify_sha256(&path, &got.to_uppercase()).is_ok());
assert!(verify_sha256(&path, "deadbeef").is_err());
let _ = std::fs::remove_file(&path);
}
#[test]
fn ed25519_accepts_valid_and_rejects_tampered_files() {
let fixture = test_support::Ed25519Fixture::new(b"abc");
assert!(verify_ed25519(&fixture.path, &fixture.public_key, &fixture.signature).is_ok());
std::fs::write(&fixture.path, b"abd").unwrap();
assert!(matches!(
verify_ed25519(&fixture.path, &fixture.public_key, &fixture.signature),
Err(Error::Signature(_))
));
}
#[test]
fn ed25519_rejects_malformed_keys_and_signatures() {
let fixture = test_support::Ed25519Fixture::new(b"abc");
for (key, sig) in [
("not base64!", fixture.signature.as_str()),
("AAAA", fixture.signature.as_str()),
(fixture.public_key.as_str(), "AAAA"),
] {
assert!(matches!(
verify_ed25519(&fixture.path, key, sig),
Err(Error::Signature(_))
));
}
}
#[test]
fn ed25519_verifies_signatures_made_by_sparkle_sign_update() {
const SPARKLE_PUBLIC_KEY: &str = "6kpsY+KcUgq+9VB7Ey7F+ZVHdq6+vnuSQh7qaRRG0iw=";
const SPARKLE_SIGNATURE: &str = "fkZSsqZWXCJYAnlGh01EaGCGfLf0DLUpVDF6d7qpDPyexRhMbj85AAF61DR7ksXSCa/Yw1lZk6kDSYJOvioFAQ==";
let payload: Vec<u8> = (0..70 * 1024u32)
.map(|i| u8::try_from((i * 31 + 7) % 251).unwrap())
.collect();
let path = temp_file(&payload);
assert!(verify_ed25519(&path, SPARKLE_PUBLIC_KEY, SPARKLE_SIGNATURE).is_ok());
let mut tampered = payload;
tampered[CHUNK] ^= 1;
std::fs::write(&path, &tampered).unwrap();
assert!(verify_ed25519(&path, SPARKLE_PUBLIC_KEY, SPARKLE_SIGNATURE).is_err());
let _ = std::fs::remove_file(&path);
}
}