use crate::block::rounds;
use crate::ubc_check::RecompressFrom;
use crate::{BLOCK_SIZE, Schedule};
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
mod armv8;
mod scalar;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
mod x86;
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
fn has_sha1_instructions() -> bool {
#[cfg(feature = "std")]
{
std::arch::is_aarch64_feature_detected!("sha2")
}
#[cfg(not(feature = "std"))]
{
cfg!(target_feature = "sha2")
}
}
#[derive(Clone, Copy)]
enum Repr {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
ShaNi(x86::ShaNi),
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
Armv8,
#[cfg(all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sse2"
))]
Sse2,
Scalar,
}
#[derive(Clone, Copy)]
pub(crate) struct Backend(Repr);
impl Backend {
pub(crate) fn new() -> Self {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
if let Some(sha_ni) = x86::ShaNi::detect() {
return Self(Repr::ShaNi(sha_ni));
}
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
if has_sha1_instructions() {
return Self(Repr::Armv8);
}
#[cfg(all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sse2"
))]
return Self(Repr::Sse2);
#[cfg(not(all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sse2"
)))]
Self(Repr::Scalar)
}
pub(crate) fn scalar() -> Self {
Self(Repr::Scalar)
}
#[inline]
pub(crate) fn compress_spill(
&self,
state: &mut [u32; 5],
block: &[u8; BLOCK_SIZE],
m1: &mut Schedule,
state_58: &mut [u32; 5],
state_65: &mut [u32; 5],
) {
match self.0 {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Repr::ShaNi(sha_ni) => sha_ni.compress_spill(state, block, m1, state_58, state_65),
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
Repr::Armv8 => {
unsafe { armv8::compress_spill(state, block, m1, state_58, state_65) };
}
#[cfg(all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sse2"
))]
Repr::Sse2 => {
let mut block_u32 = [0u32; BLOCK_SIZE / 4];
read_block(block, &mut block_u32);
x86::sse2::compress_spill(state, &block_u32, m1, state_58, state_65);
}
Repr::Scalar => {
let mut block_u32 = [0u32; BLOCK_SIZE / 4];
read_block(block, &mut block_u32);
scalar::compress_spill(state, &block_u32, m1, state_58, state_65);
}
}
}
pub(crate) fn ensure_states(
&self,
m1: &Schedule,
need_58: bool,
state_58: &mut [u32; 5],
state_65: &mut [u32; 5],
) {
match self.0 {
#[cfg(all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sse2"
))]
Repr::Sse2 => {}
Repr::Scalar => {
let _ = (m1, need_58, state_58, state_65);
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Repr::ShaNi(_) => {
for (s, t) in [(&mut *state_58, 60), (&mut *state_65, 64)] {
*s = [s[3], s[2], s[1], s[0], s[4].wrapping_sub(m1[t])];
}
rounds::states_from_60_64(m1, need_58, state_58, state_65);
}
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
Repr::Armv8 => rounds::states_from_60_64(m1, need_58, state_58, state_65),
}
}
#[inline]
pub(crate) fn is_attack(
&self,
step: RecompressFrom,
m1: &Schedule,
dm: &[u32; 80],
state: &[u32; 5],
chaining_out: &[u32; 5],
) -> bool {
match self.0 {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Repr::ShaNi(sha_ni) => sha_ni.recompress(step, m1, dm, state, chaining_out),
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
Repr::Armv8 => unsafe { armv8::recompress(step, m1, dm, state, chaining_out) },
#[cfg(all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sse2"
))]
Repr::Sse2 => is_attack_backwards(step, m1, dm, state, chaining_out),
Repr::Scalar => is_attack_backwards(step, m1, dm, state, chaining_out),
}
}
}
#[inline]
fn is_attack_backwards(
step: RecompressFrom,
m1: &Schedule,
dm: &[u32; 80],
state: &[u32; 5],
chaining_out: &[u32; 5],
) -> bool {
let (mut ihv2, mut ends_on) = ([0u32; 5], [0u32; 5]);
rounds::recompression_step(step, &mut ihv2, &mut ends_on, m1, dm, state);
super::xor(&ends_on, chaining_out) == 0
}
#[inline(always)]
pub(crate) fn read_block(block: &[u8; BLOCK_SIZE], out: &mut [u32; BLOCK_SIZE / 4]) {
for (o, chunk) in out.iter_mut().zip(block.chunks_exact(4)) {
*o = u32::from_be_bytes(chunk.try_into().unwrap());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn is_hardware(backend: &Backend) -> bool {
match backend.0 {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Repr::ShaNi(_) => true,
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
Repr::Armv8 => true,
#[cfg(all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sse2"
))]
Repr::Sse2 => false,
Repr::Scalar => false,
}
}
#[cfg(feature = "std")]
fn name(backend: &Backend) -> &'static str {
match backend.0 {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Repr::ShaNi(_) => "sha-ni",
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
Repr::Armv8 => "armv8",
#[cfg(all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sse2"
))]
Repr::Sse2 => "sse2",
Repr::Scalar => "scalar",
}
}
#[cfg(feature = "std")]
#[test]
fn the_expected_implementation_was_selected() {
let expected = std::env::var("SHA1DC_EXPECT_BLOCK").unwrap_or_default();
let expected = expected.trim();
if expected.is_empty() {
return; }
assert_eq!(
name(&Backend::new()),
expected,
"this job was meant to exercise a different implementation of `block`"
);
}
#[test]
fn hardware_is_used_when_the_target_guarantees_it() {
let guaranteed = cfg!(any(
all(
target_arch = "aarch64",
target_feature = "neon",
target_feature = "sha2"
),
all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sha",
target_feature = "sse2",
target_feature = "ssse3",
target_feature = "sse4.1"
)
));
if guaranteed {
assert!(
is_hardware(&Backend::new()),
"cpu feature detection missed instructions the target guarantees"
);
}
}
#[cfg(feature = "std")]
mod properties {
use super::*;
use quickcheck::QuickCheck;
#[test]
fn a_backend_accepts_only_what_the_way_back_finds() {
fn prop(words: [u32; 80], state: [u32; 5], nudge: u8) -> bool {
let backend = Backend::new();
let m1 = Schedule::from_words(words);
crate::ubc_check::SHA1_DVS.iter().all(|dv| {
let (mut ihvin, mut ihvout) = ([0u32; 5], [0u32; 5]);
rounds::recompression_step(
dv.recompress_from,
&mut ihvin,
&mut ihvout,
&m1,
&dv.dm,
&state,
);
let accepts =
backend.is_attack(dv.recompress_from, &m1, &dv.dm, &state, &ihvout);
let mut wrong = ihvout;
wrong[nudge as usize % 5] ^= 1 << u32::from(nudge % 32);
let rejects =
!backend.is_attack(dv.recompress_from, &m1, &dv.dm, &state, &wrong);
accepts && rejects
})
}
QuickCheck::new()
.tests(200)
.quickcheck(prop as fn([u32; 80], [u32; 5], u8) -> bool);
}
#[test]
fn hardware_agrees_with_scalar_on_any_block() {
if !is_hardware(&Backend::new()) {
return;
}
fn prop(block: [u8; BLOCK_SIZE], ihv: [u32; 5]) -> bool {
let (mut hw_state, mut hw_w) = (ihv, Schedule::zeroed());
let (mut hw_s58, mut hw_s65) = ([0u32; 5], [0u32; 5]);
Backend::new().compress_spill(
&mut hw_state,
&block,
&mut hw_w,
&mut hw_s58,
&mut hw_s65,
);
let (mut sc_state, mut sc_w) = (ihv, Schedule::zeroed());
let (mut s58, mut s65) = ([0u32; 5], [0u32; 5]);
Backend(Repr::Scalar).compress_spill(
&mut sc_state,
&block,
&mut sc_w,
&mut s58,
&mut s65,
);
let mut m = [0u32; 16];
read_block(&block, &mut m);
let want = Schedule::expand(&m);
Backend::new().ensure_states(&hw_w, true, &mut hw_s58, &mut hw_s65);
hw_state == sc_state
&& hw_w.words() == sc_w.words()
&& hw_w.words() == want.words()
&& hw_s58 == s58
&& hw_s65 == s65
}
QuickCheck::new()
.tests(1_000)
.quickcheck(prop as fn([u8; BLOCK_SIZE], [u32; 5]) -> bool);
}
#[test]
fn state_recovery_agrees_on_any_block() {
fn prop(m: [u32; 16], ihv: [u32; 5]) -> bool {
let mut replayed = ihv;
let (mut w, mut want_58, mut want_65) = (Schedule::zeroed(), [0u32; 5], [0u32; 5]);
scalar::compress_spill(&mut replayed, &m, &mut w, &mut want_58, &mut want_65);
let at = |t| rounds::state_at(&ihv, &w, t);
let (mut from_60, mut from_64) = (at(60), at(64));
rounds::states_from_60_64(&w, true, &mut from_60, &mut from_64);
let (mut skipped_58, mut only_65) = ([0xDEAD_BEEFu32; 5], at(64));
rounds::states_from_60_64(&w, false, &mut skipped_58, &mut only_65);
at(58) == want_58
&& at(65) == want_65
&& from_60 == want_58
&& from_64 == want_65
&& only_65 == want_65
&& skipped_58 == [0xDEAD_BEEF; 5]
}
QuickCheck::new()
.tests(1_000)
.quickcheck(prop as fn([u32; 16], [u32; 5]) -> bool);
}
}
}