-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild-crx.py
More file actions
160 lines (128 loc) · 4.78 KB
/
build-crx.py
File metadata and controls
160 lines (128 loc) · 4.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env python3
"""Build a CRX3 extension package from the dist/ folder."""
import hashlib
import io
import os
import struct
import sys
import zipfile
from pathlib import Path
try:
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
except ImportError:
import subprocess
subprocess.check_call([sys.executable, "-m", "pip", "install", "--user", "cryptography"])
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
REPO = Path(__file__).parent
DIST = REPO / "dist"
KEY_PATH = REPO / "discrub.pem"
OUT_CRX = REPO / "Discrub-v2.10.0.crx"
def load_or_create_key():
if KEY_PATH.exists():
with open(KEY_PATH, "rb") as f:
return serialization.load_pem_private_key(f.read(), password=None)
print(f"[*] Generating new RSA-2048 key at {KEY_PATH}")
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
with open(KEY_PATH, "wb") as f:
f.write(
key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
)
return key
def zip_dir(src_dir: Path) -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
for root, _dirs, files in os.walk(src_dir):
for name in files:
full = Path(root) / name
rel = full.relative_to(src_dir).as_posix()
zf.write(full, rel)
return buf.getvalue()
# Protobuf varint encoder
def varint(value: int) -> bytes:
out = bytearray()
while True:
b = value & 0x7F
value >>= 7
if value:
out.append(b | 0x80)
else:
out.append(b)
break
return bytes(out)
def tag(field: int, wire: int) -> bytes:
return varint((field << 3) | wire)
def length_delimited(field: int, data: bytes) -> bytes:
return tag(field, 2) + varint(len(data)) + data
def build_signed_data(crx_id: bytes) -> bytes:
# SignedData { bytes crx_id = 1; }
return length_delimited(1, crx_id)
def build_asymmetric_key_proof(pubkey_der: bytes, signature: bytes) -> bytes:
# AsymmetricKeyProof { bytes public_key = 1; bytes signature = 2; }
return length_delimited(1, pubkey_der) + length_delimited(2, signature)
def build_crx_file_header(pubkey_der: bytes, signature: bytes, signed_header_data: bytes) -> bytes:
# CrxFileHeader {
# repeated AsymmetricKeyProof sha256_with_rsa = 2;
# bytes signed_header_data = 10000;
# }
proof = build_asymmetric_key_proof(pubkey_der, signature)
out = length_delimited(2, proof)
# Field 10000, wire type 2: tag bytes are 0x82 0xf1 0x04
out += b"\x82\xf1\x04" + varint(len(signed_header_data)) + signed_header_data
return out
def main():
if not DIST.exists():
print(f"[!] dist/ folder not found at {DIST}")
sys.exit(1)
key = load_or_create_key()
pubkey = key.public_key()
pubkey_der = pubkey.public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
# crx_id = first 16 bytes of SHA-256 of DER pubkey
crx_id = hashlib.sha256(pubkey_der).digest()[:16]
print(f"[*] CRX ID (first 16 bytes of pubkey SHA-256): {crx_id.hex()}")
# Signed header data = SignedData protobuf containing crx_id
signed_header_data = build_signed_data(crx_id)
# ZIP the dist folder
print(f"[*] Zipping {DIST}")
zip_bytes = zip_dir(DIST)
print(f"[*] ZIP size: {len(zip_bytes)} bytes")
# Build signed payload: "CRX3 SignedData\x00" + LE32(len) + signed_header_data + zip_bytes
signed_payload = (
b"CRX3 SignedData\x00"
+ struct.pack("<I", len(signed_header_data))
+ signed_header_data
+ zip_bytes
)
# Sign with RSA-SHA256 PKCS1v15
signature = key.sign(
signed_payload,
padding.PKCS1v15(),
hashes.SHA256(),
)
print(f"[*] Signature length: {len(signature)} bytes")
# Build the CrxFileHeader
header = build_crx_file_header(pubkey_der, signature, signed_header_data)
print(f"[*] CrxFileHeader size: {len(header)} bytes")
# Assemble the CRX file
# Magic 'Cr24' + version 3 + header_size + header + zip
crx = (
b"Cr24"
+ struct.pack("<I", 3)
+ struct.pack("<I", len(header))
+ header
+ zip_bytes
)
with open(OUT_CRX, "wb") as f:
f.write(crx)
print(f"[+] Wrote {OUT_CRX} ({len(crx)} bytes)")
print(f"[+] Magic bytes: {crx[:8].hex()} (expected: 43723234 03000000)")
if __name__ == "__main__":
main()