initial commit
This commit is contained in:
commit
cf172f0bde
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
target/
|
100
Cargo.lock
generated
Normal file
100
Cargo.lock
generated
Normal 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
11
Cargo.toml
Normal 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
184
src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue