1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Originally based on the hacky phpass, but they seemed to be
// imitating the non-standard FreeBSD multipass md5, as described:
// https://docs.rs/pwhash/0.3.0/pwhash/md5_crypt/index.html
// except instead of:
// $1${salt}${checksum}
// We look like:
// $P$[passes; 1][salt; 8]{checksum}
pub mod error;
use error::Error;
use base64;
use md5;
use std::convert::{TryFrom, TryInto};

#[derive(Debug)]
pub struct PhPass<'a> {
    passes: usize,
    salt: &'a str,
    // This will always match 16-bytes, however long it's encoded,
    // because that's how big an MD5 sum is
    hash: [u8; 16],
}

// It'd be nice if the base64 crate gave me access to this.
const CRYPT: &str = r"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

impl<'a> TryFrom<&'a str> for PhPass<'a> {
    // TODO Make a better error
    type Error = Error;

    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
        // TODO: For full old-WordPress support, allow 32-bit hashes
        if s.len() < 34 {
            return Err(Error::OldWPFormat);
        }

        // TODO: were this part of a suite of crypto algos, this ID would
        // choose PHPass.
        if &s[0..3] != "$P$" {
            return Err(Error::InvalidId(s[0..3].to_string()));
        }

        // 4th character, decoded on table, as a power of 2
        // TODO: access the table directly and avoid this overhead,
        // since it's only 1 character
        let passes = s.chars().nth(3);
        let passes = 1
            << CRYPT
                .find(passes.ok_or(Error::InvalidPasses(passes))?)
                .ok_or(Error::InvalidPasses(passes))?;

        // We pad by 0s, encoded as .
        let encoded = &s[12..];
        let len = encoded.len();
        let hash = base64::decode_config(
            std::iter::repeat(b'.')
                // Base64 encodes on 3-byte boundaries
                .take(3 - len % 3)
                .chain(encoded.bytes().rev())
                .collect::<Vec<_>>(),
            base64::CRYPT,
        )?
        .iter()
        // Then those backwards-fed inputs need their outputs reversed.
        .rev()
        .take(16)
        .copied()
        .collect::<Vec<_>>()
        .as_slice()
        .try_into()?;

        Ok(Self {
            passes,
            salt: &s[4..12],
            hash,
        })
    }
}

impl PhPass<'_> {
   pub fn verify<T: AsRef<[u8]>>(&self, pass: T) -> Result<(), Error> {
        let pass = pass.as_ref();
        let salt = self.salt.as_bytes();
        let checksum = (0..self.passes).fold(md5::compute([salt, pass].concat()), |a, _| {
            md5::compute([&a.0, pass].concat())
        });

        if self.hash == checksum.0 {
            Ok(())
        } else {
            Err(Error::VerificationError)
        }
    }
}