Skip to content

Instantly share code, notes, and snippets.

@roccodev
Last active August 29, 2024 12:09
Show Gist options
  • Save roccodev/8fa130f1946f89702f799f89b8469bc9 to your computer and use it in GitHub Desktop.
Save roccodev/8fa130f1946f89702f799f89b8469bc9 to your computer and use it in GitHub Desktop.
Minecraft SHA-1 complement hash calculation in Rust
// Copyright (C) 2019 RoccoDev
// Licensed under the MIT license.
// <https://opensource.org/licenses/MIT>
// Bench results:
// First hash: 152ms
// Second hash: 1ms
// Third hash: 0ms
extern crate crypto; // Tested with 0.2.36
extern crate num_bigint; // Tested with 0.2
extern crate rustc_serialize; // Tested with ^0.3
extern crate regex; // Tested with 1
use regex::Regex;
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use std::iter;
use rustc_serialize::hex::ToHex;
const LEADING_ZERO_REGEX: &str = r#"^0+"#;
fn calc_hash(name: &str) -> String {
let mut hasher = Sha1::new();
hasher.input_str(name);
let mut hex: Vec<u8> = iter::repeat(0).take((hasher.output_bits() + 7)/8).collect();
hasher.result(&mut hex);
let negative = (hex[0] & 0x80) == 0x80;
let regex = Regex::new(LEADING_ZERO_REGEX).unwrap();
if negative {
two_complement(&mut hex);
format!("-{}", regex.replace(hex.as_slice().to_hex().as_str(), "").to_string())
}
else {
regex.replace(hex.as_slice().to_hex().as_str(), "").to_string()
}
}
fn two_complement(bytes: &mut Vec<u8>) {
let mut carry = true;
for i in (0..bytes.len()).rev() {
bytes[i] = !bytes[i] & 0xff;
if carry {
carry = bytes[i] == 0xff;
bytes[i] = bytes[i] + 1;
}
}
}
mod tests {
#[test]
pub fn calc_hashes() {
assert_eq!("-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1", crate::calc_hash("jeb_"));
assert_eq!("4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48", crate::calc_hash("Notch"));
assert_eq!("88e16a1019277b15d58faf0541e11910eb756f6", crate::calc_hash("simon"));
}
}
@Sebaxus
Copy link

Sebaxus commented Dec 31, 2020

When the the hash start with a '0' this is removed like for my name: "RedstoneHero"
does this need to be added again or?

@rj00a
Copy link

rj00a commented Feb 19, 2022

For future readers: You can do this in a single line of code with the num-bigint crate.

let hex = BigInt::from_signed_bytes_be(&Sha1::digest("jeb_")).to_str_radix(16);
assert_eq!(hex, "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1");

@Robocraft999
Copy link

Robocraft999 commented Feb 4, 2023

For future readers: You can do this in a single line of code with the num-bigint crate.

let hex = BigInt::from_signed_bytes_be(&Sha1::digest("jeb_")).to_str_radix(16);
assert_eq!(hex, "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1");

Does this still work? For me it says:
function or associated item not found in 'Sha1' refering to Sha1::digest()
Im using rust-crypto 0.2.36 and num-bigint 0.4

@rj00a
Copy link

rj00a commented Feb 4, 2023

Does this still work? For me it says: function or associated item not found in 'Sha1' refering to Sha1::digest() Im using rust-crypto 0.2.36 and num-bigint 0.4

Is the Digest trait in scope?

@SamHDev
Copy link

SamHDev commented Apr 22, 2023

This might be better...

fn notchian_digest(mut array: [u8; 20]) -> String {
    let mut hex;

    if array[0] & 0b1000_0000 != 0 {
        hex = String::with_capacity(41);
        hex.push('-');
        array[0] &= 0b0111_1111;
    } else {
        hex = String::with_capacity(40);
    }

    for byte in array {
        write!(&mut hex, "{:X} ", byte)
            .expect("failed to write hex?");
    }

    hex
}

and with the hasher

use sha1::Digest;
use sha1::Sha1;
use sha1::digest::generic_array::GenericArray;

fn notchian_hash(plaintext: impl AsRef<[u8]>) -> String {
    let mut hasher = sha1::Sha1::default();
    hasher.update(plaintext.as_ref());

    let mut alloc = [0u8; 20];
    sha1::Digest::finalize_into(hasher, GenericArray::from_mut_slice(&mut alloc));
    
    notchian_digest(alloc)
}

@NathanHuisman
Copy link

NathanHuisman commented May 8, 2023

Another example (no regex needed):

use sha1::{Digest, Sha1};

pub fn calc_hash(name: &str) -> String {
    let mut hash: [u8; 20] = Sha1::new().chain_update(name).finalize().into();
    let negative = (hash[0] & 0x80) == 0x80;

    // Digest is 20 bytes, so 40 hex digits plus the minus sign if necessary.
    let mut hex = String::with_capacity(40 + negative as usize);
    if negative {
        hex.push('-');

        // two's complement
        let mut carry = true;
        for b in hash.iter_mut().rev() {
            (*b, carry) = (!*b).overflowing_add(carry as u8);
        }
    }
    hex.extend(
        hash.into_iter()
            // extract hex digits
            .flat_map(|x| [x >> 4, x & 0xf])
            // skip leading zeroes
            .skip_while(|&x| x == 0)
            .map(|x| char::from_digit(x as u32, 16).expect("x is always valid base16")),
    );
    hex
}

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test_cases() {
        let pairs = &[
            ("Notch", "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"),
            ("jeb_", "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1"),
            ("simon", "88e16a1019277b15d58faf0541e11910eb756f6"),
        ];
        for (input, output) in pairs {
            assert_eq!(&calc_hash(input), output);
        }
    }
}

@flavianz
Copy link

num-bigint

Thanks man

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment