├── sshd_for_tplink.7z
├── sshd.md
├── README.md
└── tpconf_bin_xml.py
/sshd_for_tplink.7z:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sta-c0000/tpconf_bin_xml/HEAD/sshd_for_tplink.7z
--------------------------------------------------------------------------------
/sshd.md:
--------------------------------------------------------------------------------
1 | ## Running sshd on the TP-Link TD-W9970 modem router
2 |
3 | The official OpenSSH server (sshd deamon) can be made to work on the TP-Link modem router. It consumes more RAM than dropbear, however it is the standard and can provide chrooted sftp.
4 |
5 | You can create a host key for the router on your PC (do not use passphrase for host keys!):
6 | ```sh
7 | ssh-keygen -t ed25519 -f openssh_ed25519_host_key # or any key type you want
8 | ```
9 |
10 | In your startup script on the USB drive (typically mounted at */var/usbdisk/sda1*) you will also need to add (assuming you have those directories and files on the USB drive):
11 | ```sh
12 | # copy sshd host key with correct permissions (empty dir for privilege separation user)
13 | mkdir /var/ssh /var/empty
14 | cp /var/usbdisk/sda1/ssh/openssh_ed25519_host_key /var/ssh/openssh_ed25519_host_key
15 | chmod 400 /var/ssh/openssh_ed25519_host_key
16 | # Start sshd
17 | /var/usbdisk/sda1/bin/sshd -f /var/usbdisk/sda1/ssh/sshd_config -E /var/usbdisk/sda1/log/sshd.log
18 | ```
19 | In this case, *sshd_config* file would contain ```HostKey /var/ssh/openssh_ed25519_host_key```.
20 | (Also possible to launch sshd from busybox's inetd using the -i parameter)
21 |
22 | To enable admin (root) logins using keys, you can use the following line in your *sshd_config* file (use *no* if you only want chrooted sftp, *yes* to enable admin password logins):
23 | ```
24 | PermitRootLogin prohibit-password
25 | ```
26 | Copy the contents of the public keys (.pub) you want to use to login to admin (root) to ```/var/usbdisk/sda1/root/.ssh/authorized_keys``` assuming you've also updated the passwd file's admin home directory field with ```/var/usbdisk/sda1/root/```
27 |
28 | You will need to add to your passwd file an sshd privilege separation user; and optionally an sftp user (*sftp-user* can be named whatever you want):
29 | ```
30 | sshd:x:74:74:Privilege separation user:/var/empty:nologin
31 | sftp-user:x:1001:1001:sftp user:/var/usbdisk/sda1/home/sftp:/bin/sh
32 | ```
33 | For key based password-less access, simply append the contents of the public keys (*.pub) wanting access to a ```/var/usbdisk/sda1/home/sftp/.ssh/authorized_keys``` file.
34 |
35 | If you want to enable password access for the *sftp-user*, you can simply replace the *x* (password field) with a password generated using this command on your PC:
36 | ```sh
37 | openssl passwd -1
38 | ```
39 | Your sshd_config should also contain (sftp chroot to the *sftp-root* directory on the USB drive):
40 | ```
41 | # Uncomment next line to enable sftp-user password access
42 | # PasswordAuthentication yes
43 | StrictModes no
44 | Subsystem sftp internal-sftp
45 | Match User sftp-user
46 | ChrootDirectory /var/usbdisk/sda1/sftp-root
47 | ForceCommand internal-sftp
48 | AllowTCPForwarding no
49 | X11Forwarding no
50 | ```
51 |
52 | By default the sshd server should only be visible from within the LAN. Generally you should not enable Internet (outside) access to your sshd server, however you can enable this with *iptables* commands in your startup script. If you do this you may want to consider changing the sshd port (because the default one *WILL* get probed) and maybe even use something like knockd to limit access to this port.
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tpconf_bin_xml.py
2 |
3 | Simple command line utility to convert TP-Link modem/router backup config files from binary to XML and back:
4 | - conf.bin ➡ decrypt, md5hash and uncompress ➡ conf.xml
5 | - conf.xml ➡ compress, md5hash and encrypt ➡ conf.bin
6 |
7 | *Should work for TP-Link models: TD-W8970, TD-W8980, TD-W9970, TD-W9980 (thanks d3dave), Archer VR900, C2, C20 and C60 / AC1350 (d3dave), TL-WR841N (thanks odolezal), WR841N V14 (thanks mcoops), XZ005-G6 (thanks tripleoxygen) and Archer AC3150 v2 (thanks borodean!).*
8 | *May or may not work with other TP-Link modem/routers, newer firmwares.*
9 |
10 | ## Getting Started
11 |
12 | Single python file, [download](https://github.com/sta-c0000/tpconf_bin_xml/raw/master/tpconf_bin_xml.py), optionally chmod +x, and run.
13 |
14 | First, download a backup conf.bin file from your router using its web interface (System Tools → Backup & Restore → Backup).
15 | ```sh
16 | python3 tpconf_bin_xml.py -h # show help
17 | python3 tpconf_bin_xml.py conf.bin conf.xml # convert bin to XML
18 | ```
19 | *(optionally make changes to conf.xml)*
20 | ```sh
21 | python3 tpconf_bin_xml.py conf.xml conf_new.bin # convert XML to bin
22 | ```
23 | ### Prerequisites
24 |
25 | - Python 3.x
26 | - pycryptodome
27 | - apt install python3-pycryptodome # for Debian type distro
28 | - *OR* (for other platforms)
29 | - pip install pycryptodomex
30 |
31 | ## Why?
32 |
33 | To recover your router's account/password or simply make changes to your router's configuration using the XML file.
34 |
35 | ### Exploring inside the router (advanced users *at own risk*)
36 | *Note: the following is for the TP-Link TD-W9970; Other routers may differ (e.g. CPU architecture and/or endianness, LEDs), adapt as necessary.*
37 |
38 | To explore inside your router you can start by adding the following line to the *\* section of your router's configuration XML:
39 | ```xml
40 |
41 | ```
42 | After converting to .bin and uploading this new configuration you can telnet to your router's port 1023 and login using: admin/1234. *(with admin/root access you could potentially brick your router; you should change the admin password; leaving this port open is a security risk!)*
43 |
44 | The TD-W9970's configuration XML Description val is passed as a quoted parameter when launching upnpd (regarless if disabled in config) using sh -c around 20 seconds after boot (~ 35 seconds from power on).
45 |
46 | A drive connected to the TD-W9970's USB port will be mounted around 4 seconds later. So it is possible to directly execute a script located on the USB drive during boot using something like this (note that [XML special characters](https://stackoverflow.com/questions/1091945/what-characters-do-i-need-to-escape-in-xml-documents) must be escaped, and we redirect output to null otherwise upnpd launch would wait until our script finished):
47 | ```xml
48 |
49 | ```
50 | Unfortunately TP-Link did not include ext4 filesystem support in their Linux kernel, only FAT and NTFS (fuse). Using NTFS consumes a few more MB of RAM (running ntfs-3g) than FAT32, but offers the significant advantage of supporting symbolic links and large files.
51 |
52 | You can download the latest **busybox-mips** *(if your router uses a big-endian MIPS32 CPU)* from the [busybox binaries](https://busybox.net/downloads/binaries/) repository and run it from your USB drive to have a more complete set of command line tools. To get started very quickly, even using a FAT filesystem, you could source (.) something like this:
53 |
54 | ```sh
55 | alias b='/var/usbdisk/sda1/busybox-mips'
56 | for c in $(b --list); do alias $c="b $c"; done
57 | ```
58 |
59 | SSH can be used instead of telnet to log in to your router. You can download a recent compatible (MIPS32 version 1) [**dropbear_static** ssh server compiled by Martin Cracauer](https://github.com/cracauer/mFI-mPower-updated-sshd). Follow the instructions in the README there to set up the needed host keys (on a real PC: `apt install dropbear-bin`). The router's /etc is read-only, so you'll need to start *dropbear_static* with the *-r* option for each key, pointing to your USB drive, e.g.: `-r /var/usbdisk/sda1/ssh/dropbear_ecdsa_host_key`
60 |
61 | Alternatively you can use the pre-compiled (big-endian MIPS) OpenSSH sshd daemon in this repository; read the **[sshd notes](sshd.md)** for more information.
62 |
63 | Once you've made changes to the admin password / added new accounts, simply copy the passwd file to your usb; then your startup script can copy it over during each boot, e.g.: `cp -af /var/usbdisk/sda1/etc/passwd /var/passwd`
64 |
65 | For debugging you can download a pre-compiled **gdbserver** that works on the TD-W9970 from the TL-WR940N V5's code repository at [TP-Link GPL Code Center](https://www.tp-link.com/en/support/gpl-code-center).
66 |
67 | Your (latest busybox) scripts or cross-compiled programs can also control LED lights on the TD-W9970 modem router using:
68 |
69 | ```sh
70 | printf "%02x%02x" lednum ledmode > /proc/tplink/led_ctrl
71 | ```
72 |
73 | Where *lednum* is:
74 | - 2 = Internet
75 | - 3 = WPS (padlock)
76 | - 17 = Power
77 | - 18 = USB
78 | - 20 to 23 = LAN1-4
79 |
80 | And *ledmode* is:
81 | - 0 = off
82 | - 1 = on
83 | - 3 = flash slow
84 | - 4 = flash fast
85 | - 5 = flash fast pause
86 | - 7 = flash fast 5 times + slow pause
87 |
88 | ### How do I get my favourite tool running on the router?
89 |
90 | Sadly due to lack of Broadcom xDSL support it is difficult at this time to run OpenWrt while using xDSL with this modem. However, it is possible to cross-compile many console / server programs and run them directly on this device using a script on your USB drive (see above).
91 | The TD-W9970 has a single core 600Mhz dual-CPU with ≈64MB of RAM. If you know which built-in services you do not need, you can first disable them via the web interface, then kill remaining unused processes to free up the RAM they use in order to run many of your own tools and services. For example:
92 | ```sh
93 | sleep 15 # give router opportunity to finish setting up before we cleanup and setup our own services
94 | killall -1 upnpd # SIGHUP required to kill upnpd
95 | killall telnetd dyndns noipdns cwmp ushare # etc. - make sure you do not depend on any of the services you kill
96 | iptables -D INPUT -p udp -m udp --dport 161 -j ACCEPT # example to delete firewall's SNMP ACCEPT
97 |
98 | # setup my services...
99 | export TZ=EST+05:00EDT,M3.2.0/2,M11.1.0/2 # set timezone, e.g.: EST/EDT
100 | export PATH='/var/usbdisk/sda1/bin:/sbin:/usr/sbin:/bin:/usr/bin' # insert USB drive's /bin into path
101 | cp -af /var/usbdisk/sda1/etc/passwd /var/passwd # copy modified passwd file (accounts/passwords)
102 | # can run your own binaries to background here
103 | ```
104 | The easiest way to cross compile popular tools for this device is by using [Buildroot](https://buildroot.org/). Compiling static binaries using uClibc will generate small efficient portable executables.
105 | Some Buidroot settings to use for this device are: Target options: Architecture = *MIPS (big endian)*, Binary Format = *ELF*, Architecture Variant = *Generic MIPS32*, use soft-float; Build options: strip target binaries, libraries = static only; Toolchain: C library = *uClibc-ng*.
106 |
107 | ### Converting `/etc/default_config.xml` and `/etc/reduced_data_model.xml`
108 |
109 | On the v1 firmware DES keys are stored in `libcmm.so`. The locations of the keys, the functions called, and the files targeted are (found using `radare2`):
110 | ```
111 | 478DA50FF9E3D2CB > p8 8 @0xc0000-0x21a0
112 | dm_loadCfg (/etc/default_config.xml) > dm_decryptFile
113 | dm_init (/etc/reduced_data_model.xml) > dm_decryptFile
114 | 478DA50BF9E3D2CF > p8 8 @0xf0000-0x5cf0
115 | rdp_backupCfg & rdp_restoreCfg (conf.bin) > cen_desMinDo, > cen_md5VerifyDigest, > cen_uncompressBuff
116 | rdp_saveModem3gFile > rsl_3g_saveModem3gFile
117 | ```
118 | Therefore to decrypt `default_config.xml` and `reduced_data_model.xml` after copying the files to a PC:
119 | ```sh
120 | openssl enc -d -des-ecb -nopad -K 478DA50FF9E3D2CB -in default_config.xml -out default_config_decrypted.xml
121 | openssl enc -d -des-ecb -nopad -K 478DA50FF9E3D2CB -in reduced_data_model.xml -out reduced_data_model_decrypted.xml
122 | ```
123 | Similarly, to encrypt:
124 | ```sh
125 | openssl enc -e -des-ecb -nopad -K 478DA50FF9E3D2CB -in default_config_decrypted.xml -out default_config.xml
126 | openssl enc -e -des-ecb -nopad -K 478DA50FF9E3D2CB -in reduced_data_model_decrypted.xml -out reduced_data_model.xml
127 | ```
128 | Before encryption files must be *zero* padded to a file size multiple of eight. A simple script like this could be used to do this:
129 | ```sh
130 | #!/bin/sh
131 | fsize=$(stat -c%s $1)
132 | pad=$(($fsize%8))
133 | if [ "$pad" != "0" ]; then
134 | dd if=/dev/zero bs=1 count=$((8-$pad)) | cat $1 - > $1.padded
135 | fi
136 | ```
137 |
--------------------------------------------------------------------------------
/tpconf_bin_xml.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright 2018 Alain Ducharme
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | #
18 | # Description:
19 | # Command line utility to convert TP-Link router backup config files:
20 | # - conf.bin => decrypt, md5hash and uncompress => conf.xml
21 | # - conf.xml => compress, md5hash and encrypt => conf.bin
22 |
23 | import argparse
24 | from hashlib import md5
25 | from os import path
26 | import re
27 | from struct import pack, pack_into, unpack_from
28 |
29 | from Cryptodome.Cipher import DES # apt install python3-pycryptodome (OR: pip install pycryptodomex)
30 |
31 | __version__ = '0.2.14'
32 |
33 | KEYS = {
34 | 'default': b'\x47\x8D\xA5\x0B\xF9\xE3\xD2\xCF',
35 | 'xz005-g6-un-v1': b'\x45\xEE\x92\x32\xCF\x5B\x1D\xFE',
36 | }
37 |
38 | def compress(src, skiphits=False):
39 | '''Compress buffer'''
40 | # Make sure last byte is NULL
41 | if src[-1]:
42 | src += b'\0'
43 | size = len(src)
44 | buffer_countdown = size
45 | hash_table = [0] * 0x2000
46 | dst = bytearray(0x8000) # max compressed buffer size
47 | block16_countdown = 0x10 # 16 byte blocks
48 | block16_dict_bits = 0 # bits for dictionnary bytes
49 |
50 | def put_bit(bit):
51 | nonlocal block16_countdown, block16_dict_bits, d_p, d_pb
52 | if block16_countdown:
53 | block16_countdown -= 1
54 | else:
55 | pack_into('H', dst, d_pb, block16_dict_bits)
56 | d_pb = d_p
57 | d_p += 2
58 | block16_countdown = 0xF
59 | block16_dict_bits = (bit + (block16_dict_bits << 1)) & 0xFFFF
60 |
61 | def put_dict_ld(bits):
62 | ldb = bits >> 1
63 | while True:
64 | lb = (ldb - 1) & ldb
65 | if not lb:
66 | break
67 | ldb = lb
68 | put_bit(int(ldb & bits > 0))
69 | ldb = ldb >> 1
70 | while ldb:
71 | put_bit(1)
72 | put_bit(int(ldb & bits > 0))
73 | ldb = ldb >> 1
74 | put_bit(0)
75 |
76 | def hash_key(offset):
77 | b4 = src[offset:offset+4]
78 | hk = 0
79 | for b in b4[:3]:
80 | hk = (hk + b) * 0x13d
81 | return ((hk + b4[3]) & 0x1FFF)
82 |
83 | pack_into(packint, dst, 0, size) # Store original size
84 | dst[4] = src[0] # Copy first byte
85 | buffer_countdown -= 1
86 | s_p = 1
87 | s_ph = 0
88 | d_pb = 5
89 | d_p = 7
90 |
91 | while buffer_countdown > 4:
92 | while s_ph < s_p:
93 | hash_table[hash_key(s_ph)] = s_ph
94 | s_ph += 1
95 | hit = hash_table[hash_key(s_p)]
96 | count = 0
97 | if hit:
98 | while True:
99 | if src[hit + count] != src[s_p + count]:
100 | break
101 | count += 1
102 | if count == buffer_countdown:
103 | break
104 | if count >= 4 or count == buffer_countdown:
105 | hit = s_p - hit - 1
106 | put_bit(1)
107 | put_dict_ld(count - 2)
108 | put_dict_ld((hit >> 8) + 2)
109 | dst[d_p] = hit & 0xFF
110 | d_p += 1
111 | buffer_countdown -= count
112 | s_p += count
113 | if skiphits:
114 | hash_table[hash_key(s_ph)] = s_ph
115 | s_ph += count
116 | continue
117 | put_bit(0)
118 | dst[d_p] = src[s_p]
119 | s_p += 1
120 | d_p += 1
121 | buffer_countdown -= 1
122 | while buffer_countdown:
123 | put_bit(0)
124 | dst[d_p] = src[s_p]
125 | s_p += 1
126 | d_p += 1
127 | buffer_countdown -= 1
128 | pack_into('H', dst, d_pb, (block16_dict_bits << block16_countdown) & 0xFFFF)
129 | return d_p, dst[:d_p] # size, compressed buffer
130 |
131 | def uncompress(src):
132 | '''Uncompress buffer'''
133 | block16_countdown = 0 # 16 byte blocks
134 | block16_dict_bits = 0 # bits for dictionnary bytes
135 |
136 | def get_bit():
137 | nonlocal block16_countdown, block16_dict_bits, s_p
138 | if block16_countdown:
139 | block16_countdown -= 1
140 | else:
141 | block16_dict_bits = unpack_from('H', src, s_p)[0]
142 | s_p += 2
143 | block16_countdown = 0xF
144 | block16_dict_bits = block16_dict_bits << 1
145 | return 1 if block16_dict_bits & 0x10000 else 0 # went past bit
146 |
147 | def get_dict_ld():
148 | bits = 1
149 | while True:
150 | bits = (bits << 1) + get_bit()
151 | if not get_bit():
152 | break
153 | return bits
154 |
155 | size = unpack_from(packint, src, 0)[0]
156 | dst = bytearray(size)
157 | s_p = 4
158 | d_p = 0
159 |
160 | dst[d_p] = src[s_p]
161 | s_p += 1
162 | d_p += 1
163 | while d_p < size:
164 | if get_bit():
165 | num_chars = get_dict_ld() + 2
166 | msB = (get_dict_ld() - 2) << 8
167 | lsB = src[s_p]
168 | s_p += 1
169 | offset = d_p - (lsB + 1 + msB)
170 | for i in range(num_chars):
171 | # 1 by 1 ∵ sometimes copying previously copied byte
172 | dst[d_p] = dst[offset]
173 | d_p += 1
174 | offset += 1
175 | else:
176 | dst[d_p] = src[s_p]
177 | s_p += 1
178 | d_p += 1
179 | return dst
180 |
181 | def verify(src):
182 | # Try md5 hash excluding up to last 8 (padding) bytes
183 | if not any(src[:16] == md5(src[16:len(src)-i]).digest() for i in range(8)):
184 | print('ERROR: Bad file or could not decrypt file - MD5 hash check failed!')
185 | exit()
186 |
187 | def verify_ac1350(src):
188 | length = unpack_from(packint, src, 16)[0]
189 | payload = src[20:][:length]
190 | if src[:16] != md5(payload).digest():
191 | print('ERROR: Bad file or could not decrypt file - MD5 hash check failed!')
192 | exit()
193 | return payload
194 |
195 | def verify_ac3150_v2(src):
196 | verifiable_chunk_size = unpack_from(packint, src, 20)[0]
197 | verifiable_chunk = src[16:][:verifiable_chunk_size]
198 | if src[:16] != md5(verifiable_chunk).digest():
199 | print('ERROR: Bad file or could not decrypt file - MD5 hash check failed!')
200 | exit()
201 | return src[16:20] + src[24:]
202 |
203 | def check_size_endianness(src):
204 | global packint
205 | if unpack_from(packint, src)[0] > 0x20000:
206 | packint = 'I' else '>I'
207 | if unpack_from(packint, src)[0] > 0x20000:
208 | print('ERROR: compressed size too large for a TP-Link config file!')
209 | exit()
210 | print('WARNING: wrong endianness, automatically switching. (see -h)')
211 | endianness = 'little' if packint == ' 0x20000:
229 | print('ERROR: Input file too large for a TP-Link config file!')
230 | exit()
231 | if not args.overwrite and path.exists(args.outfile):
232 | print('ERROR: Output file exists, use -o to overwrite')
233 | exit()
234 |
235 | packint = 'I'
236 |
237 | with open(args.infile, 'rb') as f:
238 | src = f.read()
239 |
240 | if src.startswith(b'\r?\n(.+)$", src, re.DOTALL)
246 | if not match:
247 | print('ERROR: Header comment not found!')
248 | exit()
249 | prefix, header, postfix = match.groups()
250 | header = bytes.fromhex(header.decode('ascii'))
251 | src = header + prefix + postfix
252 | verifiable_chunk_size, dst = compress(src, True)
253 | payload_size = dst[:4]
254 | verifiable_chunk_size += len(payload_size)
255 | dst = payload_size + pack(packint, verifiable_chunk_size) + dst[4:]
256 | md5hash = md5(dst).digest()
257 | dst = md5hash + dst
258 | elif b'1350 v' in src or b'EC230' in src or b'VR1210v' in src: # AC1350 (Archer C60) and ISP variants
259 | print('OK: AC1350 XML file - compressing, hashing and encrypting…')
260 | size, dst = compress(src, True)
261 | md5hash = md5(dst[:size]).digest()
262 | dst = md5hash + pack(packint, size) + bytes(dst)
263 | elif b'W9980' in src or b'W8980' in src:
264 | print('OK: W9980/W8980 XML file - hashing, compressing and encrypting…')
265 | md5hash = md5(src).digest()
266 | size, dst = compress(md5hash + src)
267 | elif b'W8970' in src:
268 | print('OK: W8970 XML file - hashing and encrypting…')
269 | # Make sure last byte is NULL
270 | if src[-1]:
271 | src += b'\0'
272 | md5hash = md5(src).digest()
273 | dst = md5hash + src
274 | elif b'WR841N v14' in src: # lock to v14, seems varied between versions otherwise
275 | print('OK: WR841N v14 XML file - compressing, hashing and encrypting…')
276 | if packint == '>I':
277 | print('WARNING: wrong endianness, automatically setting little. (see -h)')
278 | packint = 'I': # Archer models can be little or big-endian!
298 | print('WARNING: make sure you are using correct endianness. (see -h)')
299 | # Older Archer C2 & C20 v1 skiphits, newer v4 & v5 don't
300 | if re.search(b'Archer C2[0-9]?[A-z]? v1', src):
301 | skiphits = True
302 | print('OK: XML file - compressing, hashing and encrypting…')
303 | size, dst = compress(src, skiphits)
304 | md5hash = md5(dst[:size]).digest()
305 | dst = md5hash + bytes(dst)
306 |
307 | # data length for encryption must be multiple of 8
308 | if len(dst) & 7:
309 | dst += b'\0' * (8 - (len(dst) & 7))
310 |
311 | crypto = DES.new(key, DES.MODE_ECB)
312 | output = crypto.encrypt(bytes(dst))
313 |
314 | else:
315 | xml = None
316 | # Assuming encrypted config file
317 | if len(src) & 7: # Encrypted file length must be multiple of 8
318 | print('ERROR: Wrong input file type!')
319 | exit()
320 |
321 | for key in KEYS.values():
322 | crypto = DES.new(key, DES.MODE_ECB)
323 | decrypted = crypto.decrypt(src)
324 |
325 | if decrypted[16:21] == b'\n".encode())
361 | xml = bytes().join(lines)
362 | else:
363 | print('WARNING: Unrecognized file type when using this key! Attempting next one.')
364 | crypto = None
365 |
366 | # XML data found, decryption/decompression succeeded for this key.
367 | if xml is not None:
368 | break
369 |
370 | else:
371 | print('ERROR: Unrecognized file type and no known keys left!')
372 | exit()
373 |
374 | if args.newline:
375 | if xml[-1] == 0: # NULL
376 | xml[-1] = 0xa # LF
377 | output = xml
378 |
379 | with open(args.outfile, 'wb') as f:
380 | f.write(output)
381 | print('Done.')
382 |
--------------------------------------------------------------------------------