LEFT | RIGHT |
1 # This Source Code Form is subject to the terms of the Mozilla Public | 1 # This Source Code Form is subject to the terms of the Mozilla Public |
2 # License, v. 2.0. If a copy of the MPL was not distributed with this | 2 # License, v. 2.0. If a copy of the MPL was not distributed with this |
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. | 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
4 | 4 |
5 import base64 | |
6 import re | 5 import re |
7 import struct | 6 import struct |
8 import time | 7 import time |
9 from xml.etree import ElementTree | |
10 import zlib | 8 import zlib |
11 | 9 |
12 from Crypto.Hash import SHA | 10 from Crypto.Hash import SHA |
13 from Crypto.PublicKey import RSA | 11 from Crypto.PublicKey import RSA |
14 from Crypto.Signature import PKCS1_v1_5 | 12 from Crypto.Signature import PKCS1_v1_5 |
15 | 13 |
| 14 from buildtools.packager import getTemplate |
| 15 |
| 16 XAR_HEADER = struct.Struct('>IHHQQI') |
16 XAR_HEADER_MAGIC = 0x78617221 | 17 XAR_HEADER_MAGIC = 0x78617221 |
17 XAR_HEADER_SIZE = 28 | |
18 XAR_VERSION = 1 | 18 XAR_VERSION = 1 |
19 XAR_CKSUM_SHA1 = 1 | 19 XAR_CKSUM_SHA1 = 1 |
20 | 20 |
21 def read_key(keyfile): | 21 |
| 22 def read_certificates_and_key(keyfile): |
22 with open(keyfile, 'r') as file: | 23 with open(keyfile, 'r') as file: |
23 data = file.read() | 24 data = file.read() |
24 data = re.sub(r'(-+END PRIVATE KEY-+).*', r'\1', data, flags=re.S) | |
25 return RSA.importKey(data) | |
26 | 25 |
27 def read_certificates(keyfile): | 26 certificates = [] |
28 certs = [] | 27 key = None |
29 with open(keyfile, 'r') as file: | 28 for match in re.finditer(r'-+BEGIN (.*?)-+(.*?)-+END \1-+', data, re.S): |
30 data = file.read() | 29 section = match.group(1) |
31 for match in re.finditer(r'-+BEGIN CERTIFICATE-+(.*?)-+END CERTIFICATE-+
', data, re.S): | 30 if section == 'CERTIFICATE': |
32 certs.append(base64.b64decode(match.group(1))) | 31 certificates.append(re.sub(r'\s+', '', match.group(2))) |
33 return certs | 32 elif section == 'PRIVATE KEY': |
| 33 key = RSA.importKey(match.group(0)) |
| 34 if not key: |
| 35 raise Exception('Could not find private key in file') |
| 36 |
| 37 return certificates, key |
| 38 |
34 | 39 |
35 def get_checksum(data): | 40 def get_checksum(data): |
36 return SHA.new(data).digest() | 41 return SHA.new(data).digest() |
37 | 42 |
| 43 |
38 def get_hexchecksum(data): | 44 def get_hexchecksum(data): |
39 return SHA.new(data).hexdigest() | 45 return SHA.new(data).hexdigest() |
| 46 |
40 | 47 |
41 def get_signature(key, data): | 48 def get_signature(key, data): |
42 return PKCS1_v1_5.new(key).sign(SHA.new(data)) | 49 return PKCS1_v1_5.new(key).sign(SHA.new(data)) |
43 | 50 |
| 51 |
44 def compress_files(filedata, root, offset): | 52 def compress_files(filedata, root, offset): |
45 files = [] | 53 compressed_data = [] |
46 filedata = sorted(filedata) | 54 filedata = sorted(filedata.iteritems()) |
47 directory_stack = [{'path': '', 'element': root}] | 55 directory_stack = [('', root)] |
48 file_id = 1 | 56 file_id = 1 |
49 for path, data in filedata: | 57 for path, data in filedata: |
50 # Remove directories that are done | 58 # Remove directories that are done |
51 while True: | 59 while not path.startswith(directory_stack[-1][0]): |
52 directory = directory_stack[-1] | |
53 directory_path = directory['path'] | |
54 if path.startswith(directory_path): | |
55 break | |
56 directory_stack.pop() | 60 directory_stack.pop() |
57 | 61 |
58 # Add new directories | 62 # Add new directories |
| 63 directory_path = directory_stack[-1][0] |
59 relpath = path[len(directory_path):] | 64 relpath = path[len(directory_path):] |
60 while '/' in relpath: | 65 while '/' in relpath: |
61 directory_name, relpath = relpath.split('/', 1) | 66 name, relpath = relpath.split('/', 1) |
62 directory_path += directory_name + '/' | 67 directory_path += name + '/' |
63 element = ElementTree.SubElement(directory['element'], 'file') | |
64 directory = { | 68 directory = { |
65 'path': directory_path, | 69 'id': file_id, |
66 'element': element, | 70 'name': name, |
| 71 'type': 'directory', |
| 72 'mode': '0755', |
| 73 'children': [], |
67 } | 74 } |
68 element.set('id', str(file_id)) | |
69 file_id += 1 | 75 file_id += 1 |
70 ElementTree.SubElement(element, 'name').text = directory_name | 76 directory_stack[-1][1].append(directory) |
71 ElementTree.SubElement(element, 'type').text = 'directory' | 77 directory_stack.append((directory_path, directory['children'])) |
72 ElementTree.SubElement(element, 'mode').text = '0755' | |
73 directory_stack.append(directory) | |
74 | 78 |
75 # Add the actual file | 79 # Add the actual file |
76 element = ElementTree.SubElement(directory['element'], 'file') | 80 compressed = zlib.compress(data, 9) |
77 element.set('id', str(file_id)) | 81 file = { |
| 82 'id': file_id, |
| 83 'name': relpath, |
| 84 'type': 'file', |
| 85 'mode': '0644', |
| 86 'checksum_uncompressed': get_hexchecksum(data), |
| 87 'size_uncompressed': len(data), |
| 88 'checksum_compressed': get_hexchecksum(compressed), |
| 89 'size_compressed': len(compressed), |
| 90 'offset': offset, |
| 91 } |
78 file_id += 1 | 92 file_id += 1 |
79 ElementTree.SubElement(element, 'name').text = relpath | 93 offset += len(compressed) |
80 ElementTree.SubElement(element, 'type').text = 'file' | 94 directory_stack[-1][1].append(file) |
81 ElementTree.SubElement(element, 'mode').text = '0644' | 95 compressed_data.append(compressed) |
| 96 return compressed_data |
82 | 97 |
83 datatag = ElementTree.SubElement(element, 'data') | |
84 ElementTree.SubElement(datatag, 'extracted-checksum', {'style': 'sha1'})
.text = get_hexchecksum(data) | |
85 ElementTree.SubElement(datatag, 'size').text = str(len(data)) | |
86 | |
87 compressed = zlib.compress(data, 9) | |
88 ElementTree.SubElement(datatag, 'encoding', {'style': 'application/x-gzi
p'}) | |
89 ElementTree.SubElement(datatag, 'archived-checksum', {'style': 'sha1'}).
text = get_hexchecksum(compressed) | |
90 ElementTree.SubElement(datatag, 'offset').text = str(offset) | |
91 ElementTree.SubElement(datatag, 'length').text = str(len(compressed)) | |
92 offset += len(compressed) | |
93 | |
94 files.append(compressed) | |
95 return files | |
96 | 98 |
97 def create(archivepath, contents, keyfile): | 99 def create(archivepath, contents, keyfile): |
98 key = read_key(keyfile) | 100 certificates, key = read_certificates_and_key(keyfile) |
99 certs = read_certificates(keyfile) | 101 checksum_length = len(get_checksum('')) |
| 102 params = { |
| 103 'certificates': certificates, |
100 | 104 |
101 root = ElementTree.Element('xar') | 105 # Timestamp epoch starts at 2001-01-01T00:00:00.000Z |
102 toc = ElementTree.SubElement(root, 'toc') | 106 'timestamp_numerical': time.time() - 978307200, |
| 107 'timestamp_iso': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), |
103 | 108 |
104 creation_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) | 109 'checksum': { |
105 ElementTree.SubElement(toc, 'creation-time').text = creation_time | 110 'offset': 0, |
| 111 'size': checksum_length, |
| 112 }, |
| 113 'signature': { |
| 114 'offset': checksum_length, |
| 115 'size': len(get_signature(key, '')), |
| 116 }, |
| 117 'files': [], |
| 118 } |
106 | 119 |
107 # Timestamp epoch starts at 2001-01-01T00:00:00.000Z | 120 offset = params['signature']['offset'] + params['signature']['size'] |
108 sign_time = str(time.time() - 978307200) | 121 compressed_data = compress_files(contents, params['files'], offset) |
109 ElementTree.SubElement(toc, 'signature-creation-time').text = sign_time | |
110 | 122 |
111 offset = 0 | 123 template = getTemplate('xartoc.xml.tmpl', autoEscape=True) |
112 | 124 toc_uncompressed = template.render(params).encode('utf-8') |
113 checksum_size = len(get_checksum('')) | |
114 checksum = ElementTree.SubElement(toc, 'checksum', {'style': 'sha1'}) | |
115 ElementTree.SubElement(checksum, 'offset').text = str(offset) | |
116 ElementTree.SubElement(checksum, 'size').text = str(checksum_size) | |
117 offset += checksum_size | |
118 | |
119 signature_size = len(get_signature(key, '')) | |
120 signature = ElementTree.SubElement(toc, 'signature', {'style': 'RSA'}) | |
121 ElementTree.SubElement(signature, 'offset').text = str(offset) | |
122 ElementTree.SubElement(signature, 'size').text = str(signature_size) | |
123 offset += signature_size | |
124 | |
125 keyinfo = ElementTree.SubElement(signature, 'KeyInfo') | |
126 keyinfo.set('xmlns', 'http://www.w3.org/2000/09/xmldsig#') | |
127 x509data = ElementTree.SubElement(keyinfo, 'X509Data') | |
128 for cert in certs: | |
129 ElementTree.SubElement(x509data, 'X509Certificate').text = base64.b64enc
ode(cert) | |
130 | |
131 files = compress_files(contents.iteritems(), toc, offset) | |
132 | |
133 toc_uncompressed = ElementTree.tostring(root).encode('utf-8') | |
134 toc_compressed = zlib.compress(toc_uncompressed, 9) | 125 toc_compressed = zlib.compress(toc_uncompressed, 9) |
135 | 126 |
136 with open(archivepath, 'wb') as file: | 127 with open(archivepath, 'wb') as file: |
137 # The file starts with a minimalistic header | 128 # The file starts with a minimalistic header |
138 header = struct.pack('>IHHQQI', XAR_HEADER_MAGIC, XAR_HEADER_SIZE, | 129 file.write(XAR_HEADER.pack(XAR_HEADER_MAGIC, XAR_HEADER.size, |
139 XAR_VERSION, len(toc_compressed), len(toc_uncompressed), | 130 XAR_VERSION, len(toc_compressed), |
140 XAR_CKSUM_SHA1) | 131 len(toc_uncompressed), XAR_CKSUM_SHA1)) |
141 file.write(header) | |
142 | 132 |
143 # It's followed up with a compressed XML table of contents | 133 # It's followed up with a compressed XML table of contents |
144 file.write(toc_compressed) | 134 file.write(toc_compressed) |
145 | 135 |
146 # Now the actual data, all the offsets are in the table of contents | 136 # Now the actual data, all the offsets are in the table of contents |
147 file.write(get_checksum(toc_compressed)) | 137 file.write(get_checksum(toc_compressed)) |
148 file.write(get_signature(key, toc_compressed)) | 138 file.write(get_signature(key, toc_compressed)) |
149 for compressed in files: | 139 for blob in compressed_data: |
150 file.write(compressed) | 140 file.write(blob) |
LEFT | RIGHT |