├── .gitignore ├── Cargo.toml ├── Cargo.lock ├── README └── src ├── README ├── crypto.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qnap_fwdec" 3 | description = "QNAP firmware image encryption/decryption tool" 4 | version = "0.1.0" 5 | authors = ["Hugo Grostabussiat "] 6 | license = "GPL-3.0-or-later" 7 | readme = "README" 8 | categories = ["command-line-utilities", "cryptography"] 9 | keywords = ["qnap", "nas", "encryption", "decryption", "firmware", "tool"] 10 | repository = "https://github.com/Bonstra/qnap_fwdec" 11 | 12 | [dependencies] 13 | getopts = "0.2" 14 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "getopts" 5 | version = "0.2.21" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | dependencies = [ 8 | "unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", 9 | ] 10 | 11 | [[package]] 12 | name = "qnap_fwdec" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", 16 | ] 17 | 18 | [[package]] 19 | name = "unicode-width" 20 | version = "0.1.8" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | 23 | [metadata] 24 | "checksum getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" 25 | "checksum unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 26 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | == QNAP Firmware decryptor == 2 | 3 | This small tool can encrypt and decrypt official firmware images for QNAP NAS, 4 | provided you know the key. It is a free software implementation of the same 5 | algorithm used in the official "PC1" tool present in QNAP NAS firmwares (at 6 | least the TS-x31 series). 7 | 8 | Official firmware images downloadable from the QNAP website are partially 9 | encrypted using what appears to be a custom symmetric cipher. 10 | 11 | Metadata is appended to the bottom of the encrypted image to indicate the 12 | length of the encrypted part as well as details about the product model the 13 | firmware image targets and version information. 14 | 15 | The key is a sequence of ASCII characters. Key length must be even, otherwise 16 | the last byte is ignored. Extended ASCII (> 127) characters should not be used 17 | as they are sign-extended when - I believe - they shouldn't. 18 | 19 | The key used for official firmwares is not disclosed here. If you own a QNAP 20 | NAS, you can easily extract it yourself is you have shell access via SSH or the 21 | serial console. 22 | -------------------------------------------------------------------------------- /src/README: -------------------------------------------------------------------------------- 1 | == QNAP Firmware encryption/decryption tool == 2 | 3 | This small tool can encrypt and decrypt official firmware images for QNAP NAS, 4 | provided you know the key. It is a free software implementation of the same 5 | algorithm used in the official "PC1" tool present in QNAP NAS firmwares (at 6 | least the TS-x31 series). 7 | 8 | Official firmware images downloadable from the QNAP website are partially 9 | encrypted using what appears to be a custom symmetric cipher. 10 | 11 | Metadata is appended to the bottom of the encrypted image to indicate the 12 | length of the encrypted part as well as details about the product model the 13 | firmware image targets and version information. 14 | 15 | The key is a sequence of ASCII characters. Key length must be even, otherwise 16 | the last byte is ignored. Extended ASCII (> 127) characters should not be used 17 | as they are sign-extended when - I believe - they shouldn't. 18 | 19 | The key used for official firmwares is not disclosed here. If you own a QNAP 20 | NAS, you can easily extract it yourself is you have shell access via SSH or the 21 | serial console. 22 | -------------------------------------------------------------------------------- /src/crypto.rs: -------------------------------------------------------------------------------- 1 | pub struct CryptoContext { 2 | y: u32, 3 | z: u32, 4 | key: Vec, // Initial key value 5 | workbuf: Vec, 6 | } 7 | 8 | impl CryptoContext { 9 | pub fn new(key: &str) -> CryptoContext { 10 | let mut vec: Vec = Vec::with_capacity(key.len()); 11 | vec.extend_from_slice(key.as_bytes()); 12 | CryptoContext { 13 | y: 0, 14 | z: 0, 15 | workbuf: vec.to_vec(), 16 | key: vec, 17 | } 18 | } 19 | 20 | pub fn reset(&mut self) { 21 | self.y = 0; 22 | self.z = 0; 23 | self.workbuf.copy_from_slice(&self.key); 24 | } 25 | 26 | fn update_yz(&mut self, index: usize, ks_val: u32) { 27 | let new_y = ((ks_val & 0xffff) * 0x015a) & 0xffff; 28 | let mut new_z = index as u32 + (self.z & 0xffff); 29 | new_z &= 0xffff; 30 | 31 | if new_z != 0 { 32 | new_z = (new_z & 0xffff) * 0x4e35; 33 | new_z &= 0xffff; 34 | }; 35 | new_z += new_y & 0xffff; 36 | new_z += self.y & 0xffff; 37 | 38 | self.y = new_y; 39 | self.z = new_z; 40 | } 41 | 42 | fn assemble(&mut self) -> u16 { 43 | let hwords = self.workbuf.len() >> 1; 44 | let mut coded_result = 0u32; 45 | let mut prev_ks_val = 0u32; 46 | 47 | for round in 0..hwords { 48 | let keyhword = { 49 | // Extract low and high bytes and sign-extend them 50 | let keybyte0 = { 51 | (self.workbuf[round << 1] as i8) as i32 52 | }; 53 | let keybyte1 = { 54 | (self.workbuf[(round << 1) + 1] as i8) as i32 55 | }; 56 | ((keybyte0 << 8) + keybyte1) as u32 57 | }; 58 | let mut ks_val = prev_ks_val ^ keyhword; 59 | self.update_yz(round, ks_val); 60 | ks_val = ((ks_val & 0xffff) * 0x4e35) + 1; 61 | ks_val &= 0xffff; 62 | coded_result = coded_result ^ (ks_val ^ self.z); 63 | prev_ks_val = ks_val; 64 | } 65 | coded_result as u16 66 | } 67 | 68 | pub fn encrypt(&mut self, inbyte: u8) -> u8 { 69 | let assembled = self.assemble(); 70 | let assembled_byte = ((assembled >> 8) ^ assembled) as u8; 71 | 72 | // Scramble the key with data byte 73 | for idx in 0..self.workbuf.len() { 74 | self.workbuf[idx] ^= inbyte; 75 | } 76 | 77 | inbyte ^ assembled_byte 78 | } 79 | 80 | pub fn decrypt(&mut self, inbyte: u8) -> u8 { 81 | let assembled = self.assemble(); 82 | let assembled_byte = ((assembled >> 8) ^ assembled) as u8; 83 | let outbyte = inbyte ^ assembled_byte; 84 | 85 | // Scramble the key with data byte 86 | for idx in 0..self.workbuf.len() { 87 | self.workbuf[idx] ^= outbyte; 88 | } 89 | 90 | outbyte 91 | } 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use super::CryptoContext; 97 | 98 | // 32 bytes from /dev/urandom 99 | const SAMPLE1_PLAINTEXT: [u8; 0x20] = [ 100 | 0x5a, 0x90, 0x77, 0x24, 0xc7, 0x14, 0x37, 0xeb, 0x52, 0x5f, 101 | 0x72, 0x60, 0xbc, 0x7d, 0xb1, 0x95, 0x36, 0x99, 0x5e, 0x09, 102 | 0xe5, 0x49, 0x06, 0x5b, 0xdb, 0x3a, 0xa3, 0xd4, 0x17, 0x9a, 103 | 0xd4, 0x4a, 104 | ]; 105 | const SAMPLE1_CIPHERTEXT: [u8; 0x20] = [ 106 | 0x3f, 0xb9, 0xcf, 0xf6, 0xd1, 0x80, 0xc0, 0x4c, 0xf7, 0x40, 107 | 0xc0, 0x74, 0x98, 0xbd, 0x7e, 0x3f, 0x8f, 0xc6, 0x98, 0x92, 108 | 0x55, 0xc0, 0x09, 0xd9, 0xa7, 0x8b, 0x10, 0x90, 0xfe, 0x66, 109 | 0xc5, 0xcf 110 | ]; 111 | const SAMPLE1_KEY: &'static str = "TestKey"; 112 | 113 | #[test] 114 | fn instantiate() { 115 | CryptoContext::new("blablabla"); 116 | } 117 | 118 | #[test] 119 | fn reset() { 120 | let mut ctx: CryptoContext = CryptoContext::new("TrucMachin"); 121 | ctx.y = 30; 122 | ctx.z = 0x4809; 123 | ctx.workbuf[0] = 0xff; 124 | ctx.reset(); 125 | assert_eq!(ctx.y, 0); 126 | assert_eq!(ctx.z, 0); 127 | assert_eq!(ctx.workbuf[0], ctx.key[0]); 128 | } 129 | 130 | #[test] 131 | fn key_does_not_include_null_terminator() { 132 | let ctx: CryptoContext = CryptoContext::new("TrucMachin"); 133 | assert_eq!(ctx.key.len(), 10); 134 | } 135 | 136 | #[test] 137 | fn encrypt_sample1() { 138 | let mut ctx: CryptoContext = CryptoContext::new(SAMPLE1_KEY); 139 | let mut encbyte; 140 | for i in 0..(SAMPLE1_PLAINTEXT.len()) { 141 | encbyte = ctx.encrypt(SAMPLE1_PLAINTEXT[i]); 142 | assert_eq!(encbyte, SAMPLE1_CIPHERTEXT[i]); 143 | println!("Encrypted byte {}", i); 144 | } 145 | } 146 | 147 | #[test] 148 | fn decrypt_sample1() { 149 | let mut ctx: CryptoContext = CryptoContext::new(SAMPLE1_KEY); 150 | let mut decbyte; 151 | for i in 0..(SAMPLE1_CIPHERTEXT.len()) { 152 | decbyte = ctx.decrypt(SAMPLE1_CIPHERTEXT[i]); 153 | assert_eq!(decbyte, SAMPLE1_PLAINTEXT[i]); 154 | println!("Decrypted byte {}", i); 155 | } 156 | } 157 | 158 | #[test] 159 | fn encrypt_and_decrypt() { 160 | use std::str; 161 | let orig = "Il était un petit navire, il était un petit navire \ 162 | qui n'avait ja-ja-jamais navigué, qui n'avait ja-ja-jamais \ 163 | navigué. Oh hé, oh hé !"; 164 | let enc = { 165 | let mut ectx: CryptoContext = CryptoContext::new("SomeSecret"); 166 | let mut enc: Vec = Vec::with_capacity(orig.bytes().len()); 167 | for b in orig.bytes() { 168 | enc.push(ectx.encrypt(b)); 169 | }; 170 | enc 171 | }; 172 | let dec = { 173 | let mut dctx: CryptoContext = CryptoContext::new("SomeSecret"); 174 | let mut dec: Vec = Vec::with_capacity(enc.len()); 175 | for b in enc { 176 | dec.push(dctx.decrypt(b)); 177 | } 178 | dec 179 | }; 180 | assert_eq!(&dec, &orig.as_bytes()); 181 | } 182 | } 183 | 184 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod crypto; 2 | 3 | extern crate getopts; 4 | 5 | use crypto::CryptoContext; 6 | use getopts::Options; 7 | use std::io::prelude::*; 8 | use std::io; 9 | 10 | const DEFAULT_BLOBSIZE: usize = 0x100000; 11 | const BUFFER_SIZE: usize = 1024 * 256; 12 | 13 | #[derive(Debug)] 14 | struct EncParams { 15 | infile: String, 16 | outfile: String, 17 | key: String, 18 | blobsize: Option, 19 | modelname: Option, 20 | fwver: Option, 21 | fwdate: Option 22 | } 23 | 24 | #[derive(Debug)] 25 | struct DecParams { 26 | infile: String, 27 | outfile: String, 28 | key: String, 29 | blobsize: Option, 30 | } 31 | 32 | #[derive(Debug)] 33 | enum Mode { 34 | Encrypt(EncParams), 35 | Decrypt(DecParams), 36 | Help 37 | } 38 | 39 | #[derive(Debug)] 40 | struct FooterInfo { 41 | offset: u64, 42 | blobsize: usize, 43 | modelname: String, 44 | fwver: String, 45 | fwdate: String, 46 | } 47 | 48 | fn parse_usize(s: &str) -> Result { 49 | use std::str::FromStr; 50 | if s.starts_with("0x") { 51 | return usize::from_str_radix(&s[2..], 16); 52 | }; 53 | usize::from_str(s) 54 | } 55 | 56 | fn print_usage(program: &str, opts: &Options) { 57 | use std::path::Path; 58 | let path = Path::new(program); 59 | let file = path.file_name().unwrap_or(path.as_os_str()).to_string_lossy(); 60 | let brief = format!("Usage: {} [options] INFILE OUTFILE", file); 61 | print!("{}", opts.usage(&brief)); 62 | } 63 | 64 | fn parse_args(args: &[String]) -> Option { 65 | let mut opts = Options::new(); 66 | opts.optflag("h", "help", "print this help message"); 67 | opts.optflag("e", "encrpyt", "select encryption mode"); 68 | opts.optflag("d", "decrpyt", "select decryption mode"); 69 | opts.optopt("k", "key", "set encryption key", "KEY"); 70 | opts.optopt("s", "size", 71 | &format!("size of data to encrypt; when decrypting, override 72 | the size value read from the footer (default: 73 | 0x{:x})", DEFAULT_BLOBSIZE), "SIZE"); 74 | opts.optopt("M", "model", "set model name (with -e only)", "MODEL"); 75 | opts.optopt("V", "fwver", "set firmware version (with -e only)", 76 | "FWVER"); 77 | opts.optopt("D", "fwdate", "set firmware date (with -e only)", "FWDATE"); 78 | 79 | let matches; 80 | match opts.parse(&args[1..]) { 81 | Ok(m) => matches = m, 82 | Err(e) => { 83 | println!("{}", e); 84 | print_usage(&args[0], &opts); 85 | return None; 86 | } 87 | } 88 | 89 | if matches.opt_present("h") { 90 | print_usage(&args[0], &opts); 91 | return Some(Mode::Help); 92 | } 93 | 94 | if matches.free.len() != 2 { 95 | println!("Wrong number of arguments ({} instead of 2).", 96 | matches.free.len()); 97 | print_usage(&args[0], &opts); 98 | return None; 99 | } 100 | 101 | let key = match matches.opt_str("k") { 102 | Some(k) => k, 103 | None => { 104 | println!("No key (-k) given."); 105 | print_usage(&args[0], &opts); 106 | return None; 107 | } 108 | }; 109 | 110 | let blobsize = match matches.opt_str("s") { 111 | Some(s) => match parse_usize(&s) { 112 | Ok(s) => Some(s), 113 | Err(e) => { 114 | println!("Invalid argument for -s: {}", e); 115 | print_usage(&args[0], &opts); 116 | return None; 117 | } 118 | }, 119 | None => None 120 | }; 121 | 122 | let enc = matches.opt_present("e"); 123 | let dec = matches.opt_present("d"); 124 | if !enc && !dec { 125 | println!("One of -e or -d must be specified."); 126 | print_usage(&args[0], &opts); 127 | return None; 128 | } else if enc && dec { 129 | println!("Flags -e and -d are mutually exclusive."); 130 | print_usage(&args[0], &opts); 131 | return None; 132 | } else if enc { 133 | let params = EncParams { 134 | infile: matches.free[0].clone(), 135 | outfile: matches.free[1].clone(), 136 | key: key, 137 | blobsize: blobsize, 138 | modelname: matches.opt_str("M"), 139 | fwver: matches.opt_str("V"), 140 | fwdate: matches.opt_str("D") 141 | }; 142 | return Some(Mode::Encrypt(params)); 143 | } else { 144 | let params = DecParams { 145 | infile: matches.free[0].clone(), 146 | outfile: matches.free[1].clone(), 147 | key: key, 148 | blobsize: blobsize 149 | }; 150 | return Some(Mode::Decrypt(params)); 151 | } 152 | } 153 | 154 | fn write_footer(out: &mut T, params: &EncParams) -> io::Result<()> { 155 | // Append footer 156 | let mut footer = [0u8; 0x4a]; 157 | 158 | { 159 | let magic = &mut footer[0x0..0x6]; 160 | magic.copy_from_slice("icpnas".as_bytes()); 161 | } 162 | { 163 | let blobsize = &mut footer[0x6..0xa]; 164 | let size = params.blobsize.unwrap_or(DEFAULT_BLOBSIZE); 165 | blobsize[0] = (size as u32 & 0xff) as u8; 166 | blobsize[1] = ((size as u32 >> 8) & 0xff) as u8; 167 | blobsize[2] = ((size as u32 >> 16) & 0xff) as u8; 168 | blobsize[3] = ((size as u32 >> 24) & 0xff) as u8; 169 | } 170 | if params.modelname.is_some() { 171 | let model = &mut footer[0xa..0x1a]; 172 | let mdl = params.modelname.as_ref().unwrap().as_bytes(); 173 | let fitting_len = if mdl.len() > model.len() { 174 | model.len() 175 | } else { 176 | mdl.len() 177 | }; 178 | for i in 0..fitting_len { 179 | model[i] = mdl[i]; 180 | }; 181 | } 182 | if params.fwver.is_some() { 183 | let fwver = &mut footer[0x1a..0x2a]; 184 | let ver = params.fwver.as_ref().unwrap().as_bytes(); 185 | let fitting_len = if ver.len() > fwver.len() { 186 | fwver.len() 187 | } else { 188 | ver.len() 189 | }; 190 | for i in 0..fitting_len { 191 | fwver[i] = ver[i]; 192 | }; 193 | } 194 | if params.fwdate.is_some() { 195 | let fwdate = &mut footer[0x2a..0x4a]; 196 | let date = params.fwdate.as_ref().unwrap().as_bytes(); 197 | let fitting_len = if date.len() > fwdate.len() { 198 | fwdate.len() 199 | } else { 200 | date.len() 201 | }; 202 | for i in 0..fitting_len { 203 | fwdate[i] = date[i]; 204 | }; 205 | } 206 | 207 | try!(out.write_all(&footer)); 208 | Ok(()) 209 | } 210 | 211 | fn read_footer(reader: &mut T) -> io::Result { 212 | use std::io::SeekFrom; 213 | use std::io::{Error, ErrorKind}; 214 | let ini_pos; 215 | let footer_offset; 216 | let mut buf = [0u8; 0x4a]; 217 | 218 | ini_pos = try!(reader.seek(SeekFrom::Current(0))); 219 | footer_offset = try!(reader.seek(SeekFrom::End(-0x4a))); 220 | try!(reader.read_exact(&mut buf)); 221 | { 222 | let magic = &buf[0x0..0x6]; 223 | if magic != &b"icpnas"[..] { 224 | return Err(Error::new(ErrorKind::InvalidInput, 225 | "Incorrect magic value")); 226 | }; 227 | } 228 | let blobsize = buf[0x6] as usize | 229 | ((buf[0x7] as usize) << 8) | 230 | ((buf[0x8] as usize) << 16) | 231 | ((buf[0x9] as usize) << 24); 232 | let modelname = String::from_utf8_lossy(&buf[0xa..0x1a]); 233 | let fwver = String::from_utf8_lossy(&buf[0x1a..0x2a]); 234 | let fwdate = String::from_utf8_lossy(&buf[0x2a..0x4a]); 235 | 236 | // Seek back to initial position 237 | try!(reader.seek(SeekFrom::Start(ini_pos))); 238 | 239 | Ok(FooterInfo { 240 | offset: footer_offset, 241 | blobsize: blobsize, 242 | modelname: modelname.into_owned(), 243 | fwver: fwver.into_owned(), 244 | fwdate: fwdate.into_owned(), 245 | }) 246 | } 247 | 248 | fn encrypt(params: &EncParams) -> io::Result<()> { 249 | use std::fs::File; 250 | use std::io::BufReader; 251 | use std::io::BufWriter; 252 | 253 | let reader = { 254 | let infile = try!(File::open(¶ms.infile)); 255 | BufReader::with_capacity(BUFFER_SIZE, infile) 256 | }; 257 | let mut writer = { 258 | let outfile = try!(File::create(¶ms.outfile)); 259 | BufWriter::with_capacity(BUFFER_SIZE, outfile) 260 | }; 261 | 262 | let blobsize = params.blobsize.unwrap_or(DEFAULT_BLOBSIZE); 263 | let mut ctx = CryptoContext::new(¶ms.key); 264 | let mut written = 0usize; 265 | 266 | for inbyte in reader.bytes() { 267 | if inbyte.is_err() { 268 | return Err(inbyte.err().unwrap()); 269 | }; 270 | let outbyte = if written < blobsize { 271 | ctx.encrypt(inbyte.unwrap()) 272 | } else { 273 | inbyte.unwrap() 274 | }; 275 | try!(writer.write_all(&[outbyte])); 276 | written += 1; 277 | } 278 | 279 | match write_footer(&mut writer, params) { 280 | Ok(_) => {}, 281 | Err(err) => { return Err(err); } 282 | }; 283 | 284 | Ok(()) 285 | } 286 | 287 | fn decrypt(params: &DecParams) -> io::Result<()> { 288 | use std::fs::File; 289 | use std::io::BufReader; 290 | use std::io::BufWriter; 291 | 292 | let mut reader = { 293 | let infile = try!(File::open(¶ms.infile)); 294 | BufReader::with_capacity(BUFFER_SIZE, infile) 295 | }; 296 | let mut writer = { 297 | let outfile = try!(File::create(¶ms.outfile)); 298 | BufWriter::with_capacity(BUFFER_SIZE, outfile) 299 | }; 300 | 301 | let footer = try!(read_footer(&mut reader)); 302 | println!("Model name: {}", footer.modelname); 303 | println!("FW version: {}", footer.fwver); 304 | println!("FW date: {}", footer.fwdate); 305 | 306 | let blobsize = params.blobsize.unwrap_or(footer.blobsize); 307 | let mut ctx = CryptoContext::new(¶ms.key); 308 | let mut written = 0usize; 309 | 310 | for inbyte in reader.bytes().take(footer.offset as usize) { 311 | if inbyte.is_err() { 312 | return Err(inbyte.err().unwrap()); 313 | }; 314 | let outbyte = if written < blobsize { 315 | ctx.decrypt(inbyte.unwrap()) 316 | } else { 317 | inbyte.unwrap() 318 | }; 319 | try!(writer.write_all(&[outbyte])); 320 | written += 1; 321 | } 322 | 323 | Ok(()) 324 | } 325 | 326 | fn main() { 327 | let args: Vec = std::env::args().collect(); 328 | let mode: Mode; 329 | 330 | match parse_args(&args[..]) { 331 | Some(a) => mode = a, 332 | None => return 333 | } 334 | match mode { 335 | Mode::Encrypt(params) => { 336 | if let Err(err) = encrypt(¶ms) { 337 | println!("Encryption failed: {}", err); 338 | std::process::exit(1); 339 | }; 340 | }, 341 | Mode::Decrypt(params) => { 342 | if let Err(err) = decrypt(¶ms) { 343 | println!("Decryption failed: {}", err); 344 | std::process::exit(1); 345 | }; 346 | } 347 | Mode::Help => {} 348 | }; 349 | } 350 | 351 | --------------------------------------------------------------------------------