Repository: RustCrypto/password-hashes (Rust)

Issue:
sha-crypt: index out of bounds with invalid hashed value input
```rust
use sha_crypt::sha512_check;

fn main() {
    let hashed = "$6$rounds=656000$Ykk6fjI2sU3/uprV$Z6yV/9Z741lfroSSzB9MwxSRnGeI9Z74hBkgNsHuojQJxZ9XjPkHg9jqqGLvWZ586wqnSSx5vrXZdhrMSZZE4/";
    assert!(sha512_check("abc", hashed).is_ok());

    let hashed = "$6$rounds=656000$Ykk6fjI2sU3/uprV$Z6yV/9Z741lfroSSzB9MwxSRnGeI9Z74hBkgNsHuojQJxZ9XjPkHg9jqqGLvWZ586wqnSSx5vrXZdhrMSZZE4";
    assert!(sha512_check("abc", hashed).is_err());
}
```

First assert is OK, second hashed value input misses a `/` at the end, it will panic with index out of bounds error when running with `--release`

```
thread 'main' panicked at 'index out of bounds: the len is 85 but the index is 85', /Users/messense/.cargo/registry/src/github.com-1ecc6299db9ec823/sha-crypt-0.3.0/src/b64.rs:37:37
stack backtrace:
   0: rust_begin_unwind
             at /rustc/c8dfcfe046a7680554bf4eb612bad840e7631c4b/library/std/src/panicking.rs:515:5
   1: core::panicking::panic_fmt
             at /rustc/c8dfcfe046a7680554bf4eb612bad840e7631c4b/library/core/src/panicking.rs:92:14
   2: core::panicking::panic_bounds_check
             at /rustc/c8dfcfe046a7680554bf4eb612bad840e7631c4b/library/core/src/panicking.rs:69:5
   3: sha_crypt::b64::decode
   4: sha_crypt::sha512_check
   5: sha512_crypt::main
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
```


Discussion:
Reproduced. Thanks for the report.
This should be addressed by #242.

I can cut a release, but if you'd like to take a look first that'd be great.
Confirmed that it doesn't panic now. A new release would be awesome.
`sha-crypt` v0.3.1 released in #243 



Patch:
diff --git a/sha-crypt/src/b64.rs b/sha-crypt/src/b64.rs
index 636aea5a..eca28071 100644
--- a/sha-crypt/src/b64.rs
+++ b/sha-crypt/src/b64.rs
@@ -3,6 +3,9 @@
 use crate::defs::{MAP, TAB};
 use alloc::vec::Vec;
 
+#[cfg(feature = "simple")]
+use crate::errors::DecodeError;
+
 pub fn encode(source: &[u8]) -> Vec<u8> {
     let mut out: Vec<u8> = vec![];
     for entry in &MAP {
@@ -24,31 +27,32 @@ pub fn encode(source: &[u8]) -> Vec<u8> {
 }
 
 #[cfg(feature = "simple")]
-pub fn decode(source: &[u8]) -> Vec<u8> {
+pub fn decode(source: &[u8]) -> Result<Vec<u8>, DecodeError> {
     let mut out: [u8; 64] = [0; 64];
+
     for iter in MAP.iter().enumerate() {
         let (i, entry) = iter;
 
         let mut w: usize = 0;
 
         for k in (0..entry.3).rev() {
-            let pos = TAB
-                .iter()
-                .position(|&x| x == source[i * 4 + k as usize])
-                .unwrap();
+            let byte = source.get(i * 4 + k as usize).ok_or(DecodeError)?;
+            let pos = TAB.iter().position(|x| x == byte).ok_or(DecodeError)?;
             w <<= 6;
             w |= pos as usize;
         }
 
         out[entry.0 as usize] = (w & 0xff) as u8;
         w >>= 8;
+
         if entry.3 > 2 {
             out[entry.1 as usize] = (w & 0xff) as u8;
             w >>= 8;
             out[entry.2 as usize] = (w & 0xff) as u8;
         }
     }
-    out.to_vec()
+
+    Ok(out.to_vec())
 }
 
 mod tests {
@@ -62,9 +66,10 @@ mod tests {
             0x15, 0xdd, 0x79, 0x14, 0x45, 0xac, 0x66, 0x60, 0x25, 0x94, 0x97, 0x5e, 0x0f, 0x7f,
             0x5f, 0xaf, 0x1a, 0xe5, 0x08, 0xe7, 0x7d, 0xd4,
         ];
+
         let e = super::encode(&original);
+        let d = super::decode(&e).unwrap();
 
-        let d = super::decode(&e);
         for i in 0..d.len() {
             assert_eq!(&original[i], &d[i]);
         }
diff --git a/sha-crypt/src/errors.rs b/sha-crypt/src/errors.rs
index a3c759f2..90f7b70e 100644
--- a/sha-crypt/src/errors.rs
+++ b/sha-crypt/src/errors.rs
@@ -47,3 +47,16 @@ pub enum CheckError {
     Crypt(CryptError),
     HashMismatch,
 }
+
+/// Decoding errors.
+#[cfg(feature = "simple")]
+#[cfg_attr(docsrs, doc(cfg(feature = "simple")))]
+#[derive(Debug)]
+pub struct DecodeError;
+
+#[cfg(feature = "simple")]
+impl From<DecodeError> for CheckError {
+    fn from(_: DecodeError) -> CheckError {
+        CheckError::InvalidFormat("invalid B64".into())
+    }
+}
diff --git a/sha-crypt/src/lib.rs b/sha-crypt/src/lib.rs
index 113c0ed9..3e7787bb 100644
--- a/sha-crypt/src/lib.rs
+++ b/sha-crypt/src/lib.rs
@@ -305,7 +305,7 @@ pub fn sha512_check(password: &str, hashed_value: &str) -> Result<(), CheckError
         Err(e) => return Err(CheckError::Crypt(e)),
     };
 
-    let hash = b64::decode(hash.as_bytes());
+    let hash = b64::decode(hash.as_bytes())?;
 
     use subtle::ConstantTimeEq;
     if output.ct_eq(&hash).into() {


Test patch:
diff --git a/sha-crypt/tests/lib.rs b/sha-crypt/tests/lib.rs
index c2873455..32584a79 100644
--- a/sha-crypt/tests/lib.rs
+++ b/sha-crypt/tests/lib.rs
@@ -84,14 +84,26 @@ fn test_sha512_simple_check_roundtrip() {
 
 #[cfg(feature = "simple")]
 #[test]
-fn test_sha512_check_invalid_format() {
-    // unexpected prefix 'SHOULDNOTBEHERE'
+fn test_sha512_unexpected_prefix() {
     let pw = "foobar";
     let s = "SHOULDNOTBEHERE$6$rounds=100000$exn6tVc2j/MZD8uG$BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0";
     assert!(!sha512_check(pw, s).is_ok());
+}
 
+#[cfg(feature = "simple")]
+#[test]
+fn test_sha512_wrong_id() {
     // wrong id '7'
     let pw = "foobar";
     let s = "$7$rounds=100000$exn6tVc2j/MZD8uG$BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0";
     assert!(!sha512_check(pw, s).is_ok());
 }
+
+#[cfg(feature = "simple")]
+#[test]
+fn test_sha512_missing_trailing_slash() {
+    // Missing trailing slash
+    let pw = "abc";
+    let s = "$6$rounds=656000$Ykk6fjI2sU3/uprV$Z6yV/9Z741lfroSSzB9MwxSRnGeI9Z74hBkgNsHuojQJxZ9XjPkHg9jqqGLvWZ586wqnSSx5vrXZdhrMSZZE4";
+    assert!(!sha512_check(pw, s).is_ok());
+}