Skip to content

Instantly share code, notes, and snippets.

@camas
Created October 10, 2020 21:11
Show Gist options
  • Save camas/c70593121a691310da3188136b4ffde5 to your computer and use it in GitHub Desktop.
Save camas/c70593121a691310da3188136b4ffde5 to your computer and use it in GitHub Desktop.
Among Us `secureNew` File Format

Among Us secureNew file format

About

The secureNew file is used by Among Us to check unlocked skins, pets, etc. It has some basic anti-tamper.

Data Types

Data types used by secureNew

Name Size (bytes) Encodes Comments
packed_u32 1-5 an unsigned 32-bit integer Encoded using LEB128. Also see BinaryReader.Read7BitEncodedInt
String Varies A UTF-8 string Prefixed by it's size as a packed_u32

Format

Each byte is xor'd by it's zero-indexed offset modulo 212

After undoing the xor, the file a sequence of Strings, read until the first empty string.

The first string is the computer-specific string returned by deviceUniqueIdentifier. This is checked to detect tampering

The remaining strings are names of purchased/unlocked DLC

Example rust code

Rust Playground

use std::io::{Write, Read};

pub fn read_purchase_data(data: &[u8]) -> Vec<String> {
    // Decode
    let decoded: Vec<u8> = data
        .iter()
        .enumerate()
        .map(|(i, b)| b ^ (i % 212) as u8)
        .collect();

    let mut values = Vec::new();
    let mut r = &decoded[..];
    loop {
        let value = r.read_string();
        if value.is_empty() {
            break;
        }
        values.push(value);
    }
    values
}

pub fn write_purchase_data(values: &[String]) -> Vec<u8> {
    let mut w = Vec::<u8>::new();
    for value in values {
        w.write_string(value);
    }
    w.write_string("");
    w.into_iter()
        .enumerate()
        .map(|(i, byte)| byte ^ (i % 212) as u8)
        .collect()
}

// Read extensions
trait ReadExt {
    fn read_u8(&mut self) -> u8;
    fn read_packed_u32(&mut self) -> u32;
    fn read_string(&mut self) -> String;
}

impl ReadExt for &[u8] {
    fn read_u8(&mut self) -> u8 {
        let mut buf = [0; 1];
        self.read_exact(&mut buf).unwrap();
        buf[0]
    }
    
    fn read_packed_u32(&mut self) -> u32 {
        let mut value: u32 = 0;
        for offset in (0..).step_by(7) {
            let byte = self.read_u8();
            value |= ((byte & 0b0111_1111) as u32) << offset;
            // Return if "read next" bit unset
            if (byte & 0b1000_0000) == 0 {
                return value;
            }
        }
        unreachable!()
    }
    
    fn read_string(&mut self) -> String {
        let size = self.read_packed_u32();
        let mut buffer = vec![0; size as usize];
        self.read_exact(&mut buffer).unwrap();
        String::from_utf8(buffer).unwrap()
    }
}

trait WriteExt {
    fn write_u8(&mut self, value: u8);
    fn write_packed_u32(&mut self, value: u32);
    fn write_string(&mut self, value: &str);
}

impl WriteExt for Vec<u8> {
    fn write_u8(&mut self, value: u8) {
        self.write_all(&value.to_le_bytes()).unwrap();
    }
    
    fn write_packed_u32(&mut self, mut value: u32) {
        while value >= 128 {
            self.write_u8(value as u8 | 128);
            value >>= 7;
        }
        self.write_u8(value as u8);
    }
    
    fn write_string(&mut self, value: &str) {
        self.write_packed_u32(value.len() as u32);
        self.write_all(value.as_bytes()).unwrap();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test() {
        // Example data from a `secureNew` file
        let data = [0x28,0x67,0x67,0x65,0x33,0x64,0x67,0x36,0x3a,0x38,0x33,0x33,0x6d,0x6b,0x68,0x6a,0x24,0x25,0x70,0x75,0x2d,0x25,0x77,0x26,0x7e,0x7c,0x79,0x78,0x2c,0x7b,0x27,0x2d,0x46,0x47,0x16,0x10,0x14,0x11,0x47,0x12,0x19,0x21,0x42,0x4a,0x58,0x72,0x40,0x40,0x5e,0x54,0x3f,0x5b,0x55,0x41,0x69,0x56,0x4b,0x4d,0x48,0x54,0x52,0x5c,0x4b,0x4b,0x4f,0x29,0x23,0x37,0x1b,0x27,0x27,0x34,0x2d,0x2b,0x2b,0x27,0x20,0x2e,0x2f,0x3f,0x5d,0x39,0x33,0x27,0x0b,0x37,0x24,0x36,0x31,0x37,0x29,0x37,0x29,0x3a,0x55,0x37,0x01,0x15,0x3d,0x01,0x11,0x16,0x0e,0x0f,0x09,0x1d,0x61,0x03,0x0d,0x19,0x31,0x0c,0x11,0x01,0x06,0x12,0x1d,0x1b,0x66,0x1f,0x19,0x0d,0x25,0x1f,0x13,0x08,0x1c,0x13,0xe5,0xf5,0xed,0xf3,0xec,0xe4,0xf2,0x8a,0xe0,0xe8,0xfe,0xd4,0xea,0xe1,0xe1,0xf8,0xf5,0xe3,0xe2,0xfc,0xe0,0x9e,0xfe,0xf6,0xec,0xc6,0xfd,0xf4,0xfb,0xfa,0xf2,0xfa,0xd3,0xaa,0xca,0xc2,0xd0,0xfa,0xce,0xc6,0xda,0xcd,0xc2,0xca,0xd8,0xa1,0xc6,0xce,0xc4,0xee,0xdf,0xda,0xd8,0xdc,0xc2,0xd6,0xca,0xc0,0xb6,0xd3,0xdd,0xc9,0xe1,0xcf,0xa1,0xb1,0xa7,0xb1,0xac,0xa4,0xb2,0xcb,0xa0,0xa8,0xbe,0x94,0xbc,0xac,0xbc,0xbb,0xa9,0xb9,0xb3,0xa7,0x0a,0x69,0x63,0x77,0x5b,0x75,0x69,0x6b,0x61,0x6a,0x6f,0x04,0x64,0x6c,0x7a,0x50,0x63,0x65,0x77,0x67,0x7c,0x70,0x65,0x74,0x77,0x69,0x7f,0x11,0x74,0x7c,0x6a,0x40,0x54,0x4e,0x52,0x4b,0x45,0x51,0x29,0x4f,0x49,0x5d,0x75,0x5f,0x43,0x5a,0x4b,0x43,0x47,0x58,0x48,0x52,0x46,0x51,0x3d,0x5f,0x59,0x4d,0x65,0x49,0x49,0x4e,0x4d,0x56,0x21,0x2f,0x48,0x2b,0x25,0x31,0x19,0x31,0x21,0x22,0x23,0x25,0x2b,0x46,0x26,0x2e,0x24,0x0e,0x25,0x32,0x38,0x39,0x35,0x36,0x28,0x48,0x32,0x3a,0x28,0x2e,0x01,0x31,0x05,0x16,0x1b,0x06,0x05,0x17,0x15,0x55,0x58,0x58,0x52,0x64,0x04,0x0c,0x1a,0x30,0x07,0x19,0x1b,0x07,0x11,0x01,0x19,0x07,0x10,0x18,0x0e,0x77,0x14,0x1c,0x0a,0x0c,0xdf,0xe3,0xf7,0xed,0xe0,0xe9,0xe3,0xb5,0x88];
        
        // Read and test against known values
        let values = read_purchase_data(&data);
        assert_eq!(values, ["fef7aa12198affe44bf90a1fecc0f92ff4304a51", "hat_none", "hat_astronaut", "hat_baseballcap", "hat_brainslug", "hat_bushhat", "hat_captain", "hat_doubletophat", "hat_flowerpot", "hat_goggles", "hat_hardhat", "hat_military", "hat_paperhat", "hat_partyhat", "hat_police", "hat_stethescope", "hat_tophat", "hat_towelwizard", "hat_russian", "hat_viking", "hat_wallcap", "hats_newyears2018", "hat_whitetophat", "hats_bundle2"]);
        
        // Write and test against original
        let written = write_purchase_data(&values);
        assert_eq!(written, data);
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment