initial commit

This commit is contained in:
Rowan 2024-10-05 18:00:26 -05:00
commit cf172f0bde
4 changed files with 296 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

100
Cargo.lock generated Normal file
View file

@ -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"

11
Cargo.toml Normal file
View file

@ -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"

184
src/lib.rs Normal file
View file

@ -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<u8> {
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<Self, Self::Error> {
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<u8>,
}
impl Integrity {
pub fn new(algorithm: Algorithm, hash: impl Into<Vec<u8>>) -> Self {
Self {
algorithm,
hash: hash.into(),
}
}
pub fn from_base64(algorithm: Algorithm, base64: &str) -> Result<Self, Error> {
Ok(Self {
algorithm,
hash: Self::b64_to_hex(base64).map_err(Error::UnableToDecode)?,
})
}
fn b64_to_hex(s: &str) -> Result<Vec<u8>, 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<Self, Self::Error> {
let (algo, hash) = Self::split(value)?;
println!("{algo} {hash}");
Self::from_base64(Algorithm::try_from(algo)?, hash)
}
}
impl TryFrom<String> for Integrity {
type Error = Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
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);
}
}