Skip to content

Instantly share code, notes, and snippets.

@RavuAlHemio
Created June 20, 2025 19:18
Show Gist options
  • Save RavuAlHemio/5b687b5d58d712d37763690718d6e9ef to your computer and use it in GitHub Desktop.
Save RavuAlHemio/5b687b5d58d712d37763690718d6e9ef to your computer and use it in GitHub Desktop.
Extracting Epson POS printer firmware
//! Extracting Epson POS printer firmware.
//!
//! The .efx is a ZIP archive that contains a single .rcx file. The .rcx file begins with the byte
//! string b"Salted__" followed by 8 bytes of salt; the rest is ciphertext.
//!
//! The password for the file is generated by intermingling the bytes of the byte string
//! b"GjsnxbsfVqebuf" with the bytes of the file name within the ZIP archive, including its
//! extension, with 1 subtracted from each file name byte (e.g. b -> a, T -> S, etc.). A space byte
//! 0x20 wraps around to tilde 0x7F. This intermingling stops once either of the two strings ends.
//!
//! Examples:
//! ```plain
//! file name in ZIP file: TM-T20III_E_20.06.rcx
//! subtract 1: SL,S1/HHH^D^1/-/5-qbw
//! space out: S L , S 1 / H H H ^ D ^ 1 / - / 5 - q b w
//! spaced base password: G j s n x b s f V q e b u f
//! password: GSjLs,nSx1b/sHfHVHq^eDb^u1f/
//!
//! file name in ZIP file: L100_.rcx
//! subtract 1: K0//^-qbw
//! space out: K 0 / / ^ - q b w
//! spaced base password: G j s n x b s f V q e b u f
//! password: GKj0s/n/x^b-sqfbVw
//! ```
//!
//! The rest of the derivation is relatively straightforward:
//!
//! ```plain
//! hash1 = MD5(password || salt)
//! hash2 = MD5(hash1 || password || salt)
//! secret = hash1 || hash2
//! iv = MD5(hash2 || password || salt)
//! ```
//!
//! Finally, the secret and the IV are used in an AES256-CBC decryption with PKCS7 padding to derive
//! the plaintext from the ciphertext.
use std::io::Read;
use std::{ffi::OsString, fs::File};
use std::process::ExitCode;
use aes::Aes256;
use block_padding::Pkcs7;
use cbc;
use cipher::{BlockModeDecrypt, KeyIvInit};
use digest::DynDigest;
use md5::Md5;
use zip::ZipArchive;
const EXPECTED_START: [u8; 8] = *b"Salted__";
const SALT_LENGTH: usize = 8;
const BASE_PASSWORD: [u8; 14] = *b"GjsnxbsfVqebuf";
const MD5_HASH_SIZE: usize = 16;
fn main() -> ExitCode {
let args: Vec<OsString> = std::env::args_os().collect();
if args.len() != 3 {
eprintln!("Usage: epsonfw INFILE.efx OUTFILE.rcx");
return ExitCode::FAILURE;
}
let zip_file = File::open(&args[1])
.expect("faield to open ZIP file");
let mut zip_archive = ZipArchive::new(zip_file)
.expect("failed to read ZIP file");
// there should be one file inside
let file_name = zip_archive.file_names().nth(0)
.expect("ZIP archive is empty")
.to_owned();
// read it
let inner_bytes = {
let mut inner_file = zip_archive.by_name(&file_name)
.expect("file not found inside ZIP file?!");
let mut inner_bytes = Vec::new();
inner_file.read_to_end(&mut inner_bytes)
.expect("failed to read file in ZIP file");
inner_bytes
};
// calculate the password by intermingling:
// 1. the base password
// 2. the file name in the archive, each character - 1 (' ' - 1 wraps to '~')
// and stop when either ends
let mut password = Vec::with_capacity(BASE_PASSWORD.len() * 2);
for (&base_byte, name_byte) in BASE_PASSWORD.iter().zip(file_name.bytes()) {
password.push(base_byte);
password.push(
if name_byte <= b' ' {
b'~'
} else {
name_byte - 1
}
);
}
if !inner_bytes.starts_with(&EXPECTED_START) {
panic!("ciphertext does not start with expected header \"Salted__\"");
}
if inner_bytes.len() < EXPECTED_START.len() + SALT_LENGTH {
panic!("file is too short to get the whole salt");
}
let salt = &inner_bytes[EXPECTED_START.len()..EXPECTED_START.len()+SALT_LENGTH];
let payload = &inner_bytes[EXPECTED_START.len()+SALT_LENGTH..];
let mut md1 = Md5::default();
md1.update(&password);
md1.update(salt);
let mut h1 = [0u8; MD5_HASH_SIZE];
md1.finalize_into(&mut h1)
.expect("wrong size");
let mut md2 = Md5::default();
md2.update(&h1);
md2.update(&password);
md2.update(salt);
let mut h2 = [0u8; MD5_HASH_SIZE];
md2.finalize_into(&mut h2)
.expect("wrong size");
let mut secret = [0u8; 2*MD5_HASH_SIZE];
secret[0*MD5_HASH_SIZE..1*MD5_HASH_SIZE].copy_from_slice(&h1);
secret[1*MD5_HASH_SIZE..2*MD5_HASH_SIZE].copy_from_slice(&h2);
let mut md_iv = Md5::default();
md_iv.update(&h2);
md_iv.update(&password);
md_iv.update(salt);
let mut iv = [0u8; MD5_HASH_SIZE];
md_iv.finalize_into(&mut iv)
.expect("wrong size");
// alright then
let dec: cbc::Decryptor<Aes256> = cbc::Decryptor::new_from_slices(&secret, &iv)
.expect("wrong secret or IV size");
let mut plain_buf = vec![0u8; payload.len()];
let plain_slice = dec.decrypt_padded_b2b::<Pkcs7>(payload, &mut plain_buf)
.expect("failed to decrypt");
std::fs::write(&args[2], plain_slice)
.expect("failed to write output file");
ExitCode::SUCCESS
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment