Created
June 21, 2025 19:44
-
-
Save ketsuban/87e9c202c3fb97ce547bf55a3036aede to your computer and use it in GitHub Desktop.
Rust code for decrypting the banlist passwords in Yu-Gi-Oh! Ultimate Masters - World Championship Tournament 2006
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::collections::HashMap; | |
use std::sync::LazyLock; | |
// const CHARSET: &str = "ABCDEFGHJKLMNPQRTUVWXYZacdefghijkmnprstuvwxyz23478&#!?+-÷=%@★●▲■"; | |
// static MAPPING: LazyLock<HashMap<char, u8>> = LazyLock::new(|| { | |
// let mut map = HashMap::new(); | |
// // Each character is converted into a 6-bit value from 0 to 63. | |
// for (i, ch) in CHARSET.chars().enumerate() { | |
// map.insert(ch, i); | |
// } | |
// // I don't have noted down what these go to. I suspect the fact the map of character to | |
// // value is not injective means I felt no need to note them. I suspect they all go to 0, so | |
// // they are treated the same as A. | |
// for ch in "IOSbloq1569".chars() { | |
// map.insert(ch, 0); | |
// } | |
// map | |
// }); | |
static MAPPING: LazyLock<HashMap<char, u8>> = LazyLock::new(|| HashMap::from([ | |
('A', 0), | |
('B', 1), | |
('C', 2), | |
('D', 3), | |
('E', 4), | |
('F', 5), | |
('G', 6), | |
('H', 7), | |
('I', 0), // assumed | |
('J', 8), | |
('K', 9), | |
('L', 10), | |
('M', 11), | |
('N', 12), | |
('O', 0), // assumed | |
('P', 13), | |
('Q', 14), | |
('R', 15), | |
('S', 0), // assumed | |
('T', 16), | |
('U', 17), | |
('V', 18), | |
('W', 19), | |
('X', 20), | |
('Y', 21), | |
('Z', 22), | |
('a', 23), | |
('b', 0), // assumed | |
('c', 24), | |
('d', 25), | |
('e', 26), | |
('f', 27), | |
('g', 28), | |
('h', 29), | |
('i', 30), | |
('j', 31), | |
('k', 32), | |
('l', 0), // assumed | |
('m', 33), | |
('n', 34), | |
('o', 0), // assumed | |
('p', 35), | |
('q', 0), // assumed | |
('r', 36), | |
('s', 37), | |
('t', 38), | |
('u', 39), | |
('v', 40), | |
('w', 41), | |
('x', 42), | |
('y', 43), | |
('z', 44), | |
('1', 0), // assumed | |
('2', 45), | |
('3', 46), | |
('4', 47), | |
('5', 0), // assumed | |
('6', 0), // assumed | |
('7', 48), | |
('8', 49), | |
('9', 0), // assumed | |
('&', 50), | |
('#', 51), | |
('!', 52), | |
('?', 53), | |
('+', 54), | |
('-', 55), | |
('÷', 56), | |
('=', 57), | |
('%', 58), | |
('@', 59), | |
('★', 60), | |
('●', 61), | |
('▲', 62), | |
('■', 63), | |
])); | |
fn decrypt(password: &str) -> Vec<u8> { | |
let mut output = Vec::new(); | |
let mut accum: u16 = 0; | |
let mut slack = 0; | |
for byte in password.chars().map(|c| MAPPING[&c]) { | |
accum |= u16::from(byte) << slack; | |
slack += 6; | |
if slack >= 8 { | |
output.push(u8::try_from(accum & 0xFF).unwrap() ^ 0x39); | |
accum >>= 8; | |
slack -= 8; | |
} | |
} | |
if slack != 0 { | |
// [05:46] GenericMadScientist: If you have a byte at the end you need to pad, then that | |
// last byte is not xored | |
accum <<= 8 - slack; | |
output.push(u8::try_from(accum & 0xFF).unwrap()); | |
} else { | |
// [05:59] GenericMadScientist: Ah, looking a bit more closely, if the number of bits do fit | |
// perfectly into bytes then for the end you get an extra 0 byte, if I understand this | |
// correctly. | |
output.push(0); | |
} | |
output | |
} | |
// March 2006 | |
const PASSWORD: &str = "v2AQz8Gfz●Lup3KEWQ÷rmQ=vWGxrMrpBLeaP=rWQ@zWQ=EWQU7uMvyn?f7YG8-BWgJ?D"; | |
// September 2006 | |
// const PASSWORD: &str = "Vh7Qz8Gfz●Lrp2LAWQ@r2Q=v#Gx!MrpBdiaN=rWQ@zWQ=EWQUzmM8-C%x7uHZ4m?g7HEgsUhe÷▲EV●k4LcvD"; | |
// March 2007 | |
// const PASSWORD: &str = "HXkQz8Gfz8LEp2LNUQMrmQ=v▲Gx!MrpPdiHR=rWQ@vWQ=EWQU7uMf÷V★2k3LZ4m?gJsH▲xUheJ?DgskmQA"; | |
// September 2007 | |
// const PASSWORD: &str = "Q2aRz8Gfz8LHpsLNUQ@v4Q=v★Ge!MrpPK3H!=rWQ@rpB=XWQU7uMf÷V★2k3LZ#8XiJsHFvm4ecLH?wmmic●CV●AaKA"; | |
fn main() { | |
let data = decrypt(PASSWORD); | |
// Checksum is byte 0: if this is not equal to the bitwise negation of the sum of the other | |
// bytes modulo 256, password is rejected. | |
// [05:46] GenericMadScientist: If you have a byte at the end you need to pad, then that last | |
// byte is [...] ignored for the checksum check, and is ignored when working out the length for | |
// the length check, but is not ignored for the end when the game is looking at the remaining | |
// bytes as 2-byte values? | |
let (checksum, payload) = (&data[0], &data[1..]); | |
assert_eq!( | |
*checksum, | |
!payload.iter().cloned().fold(0, u8::wrapping_add) | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment