Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/unblob/handlers/archive/qnap/_qnap.py: 31%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

70 statements  

1# Shared infrastructure for QNAP firmware handlers. 

2import io 

3from pathlib import Path 

4 

5from unblob.file_utils import File, FileSystem, iterate_file 

6from unblob.models import ( 

7 Endian, 

8 Extractor, 

9 ExtractResult, 

10 StructParser, 

11) 

12 

13FOOTER_LEN = 74 

14 

15NAS_DEVICE_ID_PREFIX = "QNAPNAS" 

16 

17C_DEFINITIONS = """ 

18 typedef struct qnap_header { 

19 char magic[6]; 

20 uint32 encrypted_len; 

21 char device_id[16]; 

22 char file_version[16]; 

23 char firmware_date[16]; 

24 char revision[16]; 

25 } qnap_header_t; 

26""" 

27 

28 

29# https://gist.github.com/ulidtko/966277a465f1856109b2d2674dcee741#file-qnap-qts-fw-cryptor-py-L114 

30class Cryptor: 

31 def __init__(self, secret): 

32 self.secret = list(bytes(secret, "ascii")) 

33 self.n = len(secret) // 2 

34 if self.n % 2 == 0: 

35 self.secret.append(0) 

36 self.precompute_k() 

37 self.acc = 0 

38 self.y = 0 

39 self.z = 0 

40 

41 def scan(self, f, xs, s0): 

42 s = s0 

43 for x in xs: 

44 w, s = f(s, x) 

45 yield w 

46 

47 def promote(self, char): 

48 return char if char < 0x80 else char - 0x101 

49 

50 def precompute_k(self): 

51 self.k = {acc: self.table_for_acc(acc) for acc in range(256)} 

52 

53 def table_for_acc(self, a): 

54 ks = [ 

55 0xFFFFFFFF 

56 & ( 

57 (self.promote(self.secret[2 * i] ^ a) << 8) 

58 + (self.secret[2 * i + 1] ^ a) 

59 ) 

60 for i in range(self.n) 

61 ] 

62 

63 def kstep(st, q): 

64 x = st ^ q 

65 y = self.lcg(x) 

66 z = 0xFFFF & (0x15A * x) 

67 return (z, y), y 

68 

69 return list(self.scan(kstep, ks, 0)) 

70 

71 def lcg(self, x): 

72 return 0xFFFF & (0x4E35 * x + 1) 

73 

74 def kdf(self): 

75 """self.secret -> 8bit hash (+ state effects).""" 

76 tt = self.k[self.acc] 

77 res = 0 

78 for i in range(self.n): 

79 yy = self.y 

80 self.y, t2 = tt[i] 

81 self.z = 0xFFFF & (self.y + yy + 0x4E35 * (self.z + i)) 

82 res = res ^ t2 ^ self.z 

83 hi, lo = res >> 8, res & 0xFF 

84 return hi ^ lo 

85 

86 def decrypt_byte(self, v): 

87 k = self.kdf() 

88 r = 0xFF & (v ^ k) 

89 self.acc = self.acc ^ r 

90 return r 

91 

92 def decrypt_chunk(self, chunk): 

93 return bytes(map(self.decrypt_byte, chunk)) 

94 

95 

96class QnapExtractor(Extractor): 

97 def __init__(self): 

98 self._struct_parser = StructParser(C_DEFINITIONS) 

99 

100 def _get_secret(self, header) -> str: 

101 raise NotImplementedError 

102 

103 def extract(self, inpath: Path, outdir: Path) -> ExtractResult: 

104 fs = FileSystem(outdir) 

105 with ( 

106 File.from_path(inpath) as infile, 

107 fs.open(Path(f"{inpath.name}.decrypted"), "wb+") as outfile, 

108 ): 

109 infile.seek(-FOOTER_LEN, io.SEEK_END) 

110 header = self._struct_parser.parse("qnap_header_t", infile, Endian.LITTLE) 

111 cryptor = Cryptor(self._get_secret(header)) 

112 for chunk in iterate_file(infile, 0, header.encrypted_len, 1024): 

113 outfile.write(cryptor.decrypt_chunk(chunk)) 

114 for chunk in iterate_file( 

115 infile, 

116 header.encrypted_len, 

117 infile.size() - FOOTER_LEN - header.encrypted_len, 

118 1024, 

119 ): 

120 outfile.write(chunk) 

121 return ExtractResult(reports=fs.problems)