From cf172f0bde3e0911cbf8758cf395ba617ddd4097 Mon Sep 17 00:00:00 2001 From: kitsunecafe Date: Sat, 5 Oct 2024 18:00:26 -0500 Subject: [PATCH] initial commit --- .gitignore | 1 + Cargo.lock | 100 +++++++++++++++++++++++++++++ Cargo.toml | 11 ++++ src/lib.rs | 184 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 296 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ff734a7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,100 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "subresource_integrity" +version = "0.1.0" +dependencies = [ + "base64", + "sha2", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..100ee93 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "subresource_integrity" +version = "0.1.0" +edition = "2021" + +[features] +default = [] + +[dependencies] +base64 = "0.22.1" +sha2 = "0.10.8" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2c63fa1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,184 @@ +use core::fmt; + +use base64::{prelude::BASE64_STANDARD, Engine}; +use sha2::Digest; + +#[derive(Debug, PartialEq)] +pub enum Error { + InvalidSRIFormat, + AlgorithmNotSupported, + UnableToDecode(base64::DecodeError), +} + +#[derive(Debug, PartialEq)] +pub enum Algorithm { + SHA256, + SHA384, + SHA512, +} + +impl Algorithm { + pub fn digest(&self, data: impl AsRef<[u8]>) -> Vec { + match self { + Self::SHA256 => sha2::Sha256::digest(data).to_vec(), + Self::SHA384 => sha2::Sha384::digest(data).to_vec(), + Self::SHA512 => sha2::Sha512::digest(data).to_vec(), + } + } +} + +impl fmt::Display for Algorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let value = match self { + Self::SHA256 => "sha256", + Self::SHA384 => "sha384", + Self::SHA512 => "sha512", + }; + + write!(f, "{value}") + } +} + +impl TryFrom<&str> for Algorithm { + type Error = Error; + + fn try_from(value: &str) -> Result { + match value { + "sha256" => Ok(Self::SHA256), + "sha384" => Ok(Self::SHA384), + "sha512" => Ok(Self::SHA512), + _ => Err(Error::AlgorithmNotSupported), + } + } +} + +#[derive(Debug)] +pub struct Integrity { + algorithm: Algorithm, + hash: Vec, +} + +impl Integrity { + pub fn new(algorithm: Algorithm, hash: impl Into>) -> Self { + Self { + algorithm, + hash: hash.into(), + } + } + + pub fn from_base64(algorithm: Algorithm, base64: &str) -> Result { + Ok(Self { + algorithm, + hash: Self::b64_to_hex(base64).map_err(Error::UnableToDecode)?, + }) + } + + fn b64_to_hex(s: &str) -> Result, base64::DecodeError> { + BASE64_STANDARD.decode(s) + } + + pub fn verify(&self, data: impl AsRef<[u8]>) -> bool { + self.algorithm.digest(data) == self.hash + } + + fn split(value: &str) -> Result<(&str, &str), Error> { + value.split_once('-').ok_or(Error::InvalidSRIFormat) + } +} + +impl fmt::Display for Integrity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}-{}", + self.algorithm, + BASE64_STANDARD.encode(self.hash.as_slice()) + ) + } +} + +impl TryFrom<&str> for Integrity { + type Error = Error; + + fn try_from(value: &str) -> Result { + let (algo, hash) = Self::split(value)?; + println!("{algo} {hash}"); + Self::from_base64(Algorithm::try_from(algo)?, hash) + } +} + +impl TryFrom for Integrity { + type Error = Error; + + fn try_from(value: String) -> Result { + let (algo, hash) = Self::split(&value)?; + Self::from_base64(Algorithm::try_from(algo)?, hash) + } +} + +#[cfg(test)] +mod test { + use crate::Algorithm; + use crate::Error; + use crate::Integrity; + use base64::Engine; + use sha2::Digest; + + #[test] + fn try_from_sha256() { + let value = "alert('Hello, world.');"; + let digest = sha2::Sha256::digest(value); + let hash = base64::prelude::BASE64_STANDARD.encode(digest); + let integrity_value = format!("sha256-{hash}"); + let integrity = Integrity::try_from(integrity_value.as_str()).unwrap(); + + assert_eq!(integrity.algorithm, Algorithm::SHA256); + assert_eq!(integrity.hash, digest.as_slice()); + assert_eq!(integrity.to_string(), integrity_value) + } + + #[test] + fn try_from_sha384() { + let value = "alert('Hello, world.');"; + let digest = sha2::Sha384::digest(value); + let hash = base64::prelude::BASE64_STANDARD.encode(digest); + let integrity_value = format!("sha384-{hash}"); + let integrity = Integrity::try_from(integrity_value.as_str()).unwrap(); + + assert_eq!(integrity.algorithm, Algorithm::SHA384); + assert_eq!(integrity.hash, digest.as_slice()); + assert_eq!(integrity.to_string(), integrity_value); + assert!(integrity.verify(value)); + } + + #[test] + fn try_from_sha512() { + let value = "alert('Hello, world.');"; + let digest = sha2::Sha512::digest(value); + let hash = base64::prelude::BASE64_STANDARD.encode(digest); + let integrity_value = format!("sha512-{hash}"); + let integrity = Integrity::try_from(integrity_value.as_str()).unwrap(); + + assert_eq!(integrity.algorithm, Algorithm::SHA512); + assert_eq!(integrity.hash, digest.as_slice()); + assert_eq!(integrity.to_string(), integrity_value); + assert!(integrity.verify(value)); + } + + #[test] + fn bad_format() { + let value = "sha384"; + let integrity = Integrity::try_from(value); + assert_eq!(integrity.unwrap_err(), Error::InvalidSRIFormat); + } + + #[test] + fn unsupported_hash() { + let value = "alert('Hello, world.');"; + let digest = sha2::Sha224::digest(value); + let hash = base64::prelude::BASE64_STANDARD.encode(digest); + let integrity_value = format!("sha224-{hash}"); + let integrity = Integrity::try_from(integrity_value.as_str()); + assert_eq!(integrity.unwrap_err(), Error::AlgorithmNotSupported); + } +}