Project

General

Profile

Feature #90 ยป mozoid.py

jahoti, 09/13/2021 09:12 AM

 
1
#!/usr/bin/env python3
2

    
3
from cryptography.hazmat.primitives.kdf import hkdf
4
from cryptography.hazmat.backends import openssl
5
from cryptography.hazmat.primitives import hashes, hmac
6
import base64, json, librecaptcha, random, requests, sys, time
7

    
8
s, offset = requests.Session(), 0
9
s.headers.update({'user-agent': 'Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0'})
10

    
11

    
12

    
13
def get(varname):
14
	if varname not in options:
15
		options[varname] = input('Please enter ' + varname.replace('_', ' ') + ': ')
16
	
17
	if varname not in options:
18
		raise Exception('Could not obtain a value for ' + varname + '.')
19
	
20
	return options[varname]
21

    
22

    
23
def hawk_header(payload, *request): # request = (method, path, host, port)~ all str
24
	hasher, hmacer = hashes.Hash(hashes.SHA256(), openssl.backend), hmac.HMAC(key, hashes.SHA256(), openssl.backend)
25
	hasher.update(('hawk.1.payload\napplication/json\n' + payload + '\n').encode('utf-8'))
26
	
27
	payload_hash, time_stamp = base64.b64encode(hasher.finalize()).decode('utf-8'), str(int(time.time() + offset))
28
	nonce = ''.join([ random.choice('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz') for i in range(6) ])
29
	
30
	hmacer.update(('hawk.1.header\n' + time_stamp + '\n' + nonce + '\n' + '\n'.join(request) + '\n' + payload_hash + '\n\n').encode('utf-8'))
31
	mac = base64.b64encode(hmacer.finalize()).decode('utf-8')
32
	return 'Hawk id="' + key_id + '", ts="' + time_stamp + '", nonce="' + nonce + '", hash="' + payload_hash + '", mac="' + mac + '"'
33

    
34

    
35
def jwt_header():
36
	# "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" below is the base-64 encoding of the header {"alg":"HS256","typ":"JWT"}
37
	time_stamp, hmacer = int(time.time() + offset), hmac.HMAC(jwtsec, hashes.SHA256(), openssl.backend)
38
	payload = '{"iss":"' + jwtkey + '","iat":' + str(time_stamp) + ',"jti":"'
39
	payload += ''.join([ random.choice('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz') for i in range(9) ])
40
	payload +=  '","exp":' + str(time_stamp + 60) + '}'
41
	body = b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + base64.b64encode(payload.encode('utf-8'), b'-_').rstrip(b'=')
42
	hmacer.update(body)
43
	body += b'.' + base64.b64encode(hmacer.finalize(), b'-_').rstrip(b'=')
44
	return 'JWT ' + body.decode('utf-8')
45

    
46
def req(method, *args, **kwargs):
47
	r = getattr(s, method)(*args, **kwargs)
48
	if r.status_code < 200 or r.status_code >= 300:
49
		raise Exception(str(r.status_code) + ':\n' + r.text)
50

    
51
	return r
52

    
53

    
54

    
55
aliases = {
56
	'name': 'display_name',
57
	'display-name': 'display_name',
58
	'n': 'display_name',
59
	'p': 'stretched_password',
60
	'e': 'e-mail',
61
	'email': 'e-mail',
62
	'stretched-password': 'stretched_password',
63
	'password': 'stretched_password',
64
	'i': 'info',
65
	'k': 'keys',
66
	'key': 'keys',
67
	'c': 'create',
68
	'd': 'delete',
69
	'del': 'delete',
70
	'v': 'verify',
71
	'u': 'unblock',
72
	'j': 'json_key_pair',
73
	'json-key-pair': 'json_key_pair',
74
	'json-keys': 'json_key_pair',
75
	'key-pair': 'json_key_pair',
76
	'keypair': 'json_key_pair',
77
	'h': 'help'
78
}
79

    
80
long_args = {
81
	'display_name': True,
82
	'stretched_password': True,
83
	'info': True,
84
	'keys': False,
85
	'create': False,
86
	'delete': False,
87
	'verify': True,
88
	'unblock': False,
89
	'json_key_pair': True,
90
	'e-mail': True,
91
	'help': False
92
}
93

    
94

    
95

    
96

    
97
options, current_arg = {}, None
98
for arg in sys.argv[1:]:
99
	if current_arg:
100
		options[current_arg], current_arg = arg, None
101
	
102
	elif not arg.startswith('-') and len(arg) > 1:
103
		raise Exception('Positional argument found: positional arguments are not allowed.')
104
	
105
	else:
106
		if arg.startswith('--'):
107
			arg = arg[2:].lower()
108
		else:
109
			args = list(arg[1:])
110
			arg = args.pop()
111
			for arg2 in args:
112
				if arg2 not in aliases:
113
					raise Exception('Invalid short argument -' + arg2 + '.')
114
				
115
				long_arg = aliases[arg2]
116
				if long_args[long_arg]:
117
					raise Exception('Short argument -' + arg2 + ' must take an argument.')
118
				
119
				options[long_arg] = True
120
		
121
		if arg in aliases:
122
			arg = aliases[arg]
123
		
124
		elif arg not in long_args:
125
			raise Exception('Invalid argument -(-)' + arg + '.')
126
		
127
		if long_args[arg]:
128
			current_arg = arg
129
		else:
130
			options[arg] = True
131

    
132
if current_arg:
133
	raise Exception('Terminated with option name.')
134

    
135

    
136
if 'help' in options:
137
	print('''Usage:', sys.argv[0], '[OPTIONS]')
138
Available options:
139
-c, --create
140
	Generate an account or (with -k) a keypair
141
-d, --delete
142
	Delete an account (use with -k unsupported)
143
-k, --keys
144
	Perform an action on API keypair; if not specified, print the current keypair
145
-h, --help
146
	Print this help text
147

    
148
 -n, --name [DISPLAY NAME]
149
	Set the display to use when creating an account
150
 -e, --email, --e-mail [E-MAIL ADDRESS]
151
	Account e-mail address
152
-p, --stretched-password, --stretched_password [STRING]
153
	Stretched account password (a 64-character hex string)
154
-u, --unblock
155
	Unblock the account before logging in
156
''')
157
	sys.exit(0)
158

    
159

    
160
elif 'info' in options:
161
	with open(options['info'], mode='r') as f:
162
		line = f.readline()
163
		while line:
164
			if ':' in line:
165
				key, value = line.split(':', maxsplit=1)
166
				key, value = key.lower(), value[:-1]
167
				if key in aliases:
168
					key = aliases[key]
169
				
170
				if key not in options and long_args.get(key):
171
					long_args[key] = value
172
			line = f.readline()
173

    
174

    
175

    
176

    
177
if 'delete' in options or 'create' in options or 'keys' in options:
178
	email, pwd = get('e-mail'), get('stretched_password')
179
	if '@' not in email:
180
		raise Exception('Invalid e-mail address.')
181
	
182
	if len(pwd) != 64 or pwd.lstrip('1234567890abcdef'):
183
		raise Exception('Invalid stretched password: should be a 64-character hex string.')
184
	
185
	unblock = None
186
	if 'create' in options and 'keys' not in options:
187
		endpoint, verify = 'create', 'email-otp'
188
	else:
189
		endpoint, verify = 'login', None
190
		if 'verify' in options:
191
			verify = options['verify']
192
		
193
		elif 'unblock' in options:
194
			print('[Making unblock request]')
195
			req('post', 'https://api.accounts.firefox.com/v1/account/login/send_unblock_code',
196
				json={'email': email, 'metricsContent': {}})
197
			
198
			print('A code should have been sent to your e-mail address.')
199
			unblock = input('Please enter code: ')
200
			if len(unblock) != 8 or not unblock.isupper() or not unblock.isalnum():
201
				raise Exception('Invalid unblock code: should be 8 characters, either uppercase letters or digits')
202
	
203
	data = {'email': email, 'authPW': pwd, 'service': 'a4907de5fa9d78fc', 'metricsContext': {}}
204
	if verify:
205
		data['verificationMethod'] = verify
206
	
207
	if unblock:
208
		data['unblockCode'] = unblock
209
	
210
	print('[Signing in]')
211
	data = req('post', 'https://api.accounts.firefox.com/v1/account/' + endpoint, json=data).json()
212
	uid, offset = data['uid'], data['authAt'] - time.time()
213
	
214
	session_token = int(data['sessionToken'], 16)
215
	session_token = session_token.to_bytes((session_token.bit_length() + 7) // 8, 'big')
216
	key_block = hkdf.HKDF(hashes.SHA256(), 32*3*8, b'', b'identity.mozilla.com/picl/v1/sessionToken', openssl.backend).derive(session_token)
217
	# algorithm, length, salt, info, backend
218

    
219
	key_id, key = key_block[:32].hex(), key_block[32:64]
220
	
221
	# Handle verification
222
	if verify == 'email-otp':
223
		print('A six-digit verification code should have been sent to your e-mail address.')
224
		code = input('Please enter verification code: ')
225
		if not (len(code) == 6 and code.isdigit()):
226
			raise Exception('Invalid verification code: codes should be 6 digits.')
227
		
228
		data = json.dumps({'code': code, 'service': 'a4907de5fa9d78fc', 'scopes': ['profile','openid'], 'newsletters': []})
229
		auth_header = hawk_header(data, 'POST', '/v1/session/verify_code', 'api.accounts.firefox.com', '443')
230
		
231
		print('[Seeking verification]')
232
		req('post', 'https://api.accounts.firefox.com/v1/session/verify_code',
233
			headers={'authorization': auth_header, 'content-type': 'application/json'}, data=data)
234
	
235
	
236
	# Get OAuth
237
	print('[Obtaining OAuth permissions- 3 requests to be made]')
238
	r = req('get', 'https://addons.mozilla.org/api/v4/accounts/login/start/?config=amo&to=%2Fen-US%2Ffirefox%2F')
239
	state = r.url.split('&state=', maxsplit=1)[1].split('&', maxsplit=1)[0].replace('%3A', ':')
240
	
241
	data = json.dumps({'client_id': 'a4907de5fa9d78fc', 'scope': 'profile openid', 'state': state})
242
	auth_header = hawk_header(data, 'POST', '/v1/oauth/authorization', 'api.accounts.firefox.com', '443')
243
	r = req('post', 'https://api.accounts.firefox.com/v1/oauth/authorization',
244
		headers={'authorization': auth_header, 'content-type': 'application/json'}, data=data)
245
	
246
	req('get', r.json()['redirect'])
247
	
248

    
249
if 'create' in options:
250
	dbase = {'action': 'generate'}
251
	r = req('get', 'https://addons.mozilla.org/en-US/developers/addon/api/key/')
252
	csrf = r.text.replace("'", '"').split('<meta name="csrf" content="', maxsplit=1)[1].split('"', maxsplit=1)[0]
253
	if r.url == 'https://addons.mozilla.org/en-US/developers/addon/agreement/':
254
		# Part of the initial sign-up process
255
		print("LEGAL NOTICE:\nBy continuing, you accept Mozilla's:\n")
256
		print("-\tdistribution agreement <https://extensionworkshop.com/documentation/publish/firefox-add-on-distribution-agreement/>")
257
		print("-\treview policy <https://extensionworkshop.com/documentation/publish/add-on-policies/>")
258
		
259
		display_name = get('display_name')
260
		token = librecaptcha.get_token('6LcsBhYTAAAAAKpVTZ16Lh7nmTxVRKoKSr8e3vsV', 'https://addons.mozilla.org/',
261
						'Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0')
262
		
263
		data = {'distribution_agreement': 'on', 'review_policy': 'on', 'display_name': display_name, \
264
			'g-recaptcha-response': token, 'csrfmiddlewaretoken': csrf}
265
		
266
		r = req('post', 'https://addons.mozilla.org/en-US/developers/addon/agreement/', data=data,
267
			headers={'referer': 'https://addons.mozilla.org/en-US/developers/addon/agreement/'})
268
		
269
		csrf = r.text.split('<meta name="csrf" content="', maxsplit=1)[1].split('"', maxsplit=1)[0]
270
	
271
	dbase['csrfmiddlewaretoken'] = csrf
272
	req('post', 'https://addons.mozilla.org/en-US/developers/addon/api/key/',
273
		headers={'referer': 'https://addons.mozilla.org/en-US/developers/addon/api/key/'}, data=dbase)
274
	
275
	print('If you have not created a keypair before, a link should have been sent to your e-mail address.')
276
	token = input('Please enter link if applicable, nothing otherwise: ')
277
	if token:
278
		if '://' not in token:
279
			raise Exception('Invalid link.')
280
		
281
		token = token.split('?token=', maxsplit=1)[1].split('&', maxsplit=1)[0]
282
		dbase = {'confirmation_token': token, 'action': 'generate'}
283
		r = req('get', 'https://addons.mozilla.org/en-US/developers/addon/api/key/?token=' + token)
284
		dbase['csrfmiddlewaretoken'] = r.text.replace("'", '"').split('<meta name="csrf" content="', maxsplit=1)[1].split('"', maxsplit=1)[0]
285
		
286
		r = req('post', 'https://addons.mozilla.org/en-US/developers/addon/api/key/?token=' + token,
287
		headers={'referer': 'https://addons.mozilla.org/en-US/developers/addon/api/key/?token=' + token}, data=dbase)
288
	
289
	json_key, text = r.text.split('name="jwtkey" value="', maxsplit=1)[1].split('"', maxsplit=1)
290
	print('Key:', json_key)
291
	secret = text.split('name="jwtsecret" value="', maxsplit=1)[1].split('"', maxsplit=1)[0]
292
	print('Secret:', secret)
293

    
294

    
295
elif 'keys' in options:
296
	if 'delete' in options:
297
		print('KEY DELETION IS CURRENTLY UNSUPPORTED.')
298
	else:
299
		r = req('get', 'https://addons.mozilla.org/en-US/developers/addon/api/key/')
300
		json_key, text = r.text.split('name="jwtkey" value="', maxsplit=1)[1].split('"', maxsplit=1)
301
		print('Key:', json_key)
302
		secret = text.split('name="jwtsecret" value="', maxsplit=1)[1].split('"', maxsplit=1)[0]
303
		print('Secret:', secret)
304

    
305

    
306
elif 'delete' in options:
307
	if input('Please type "y" and press return to delete your account') == 'y':
308
		email, pwd = get('e-mail'), get('stretched_password')
309
		
310
		data = json.dumps({'email': email, 'authPW': pwd})
311
		auth_header = hawk_header(data, 'POST', '/v1/account/destroy', 'api.accounts.firefox.com', '443')
312
		req('post', 'https://api.accounts.firefox.com/v1/account/destroy',
313
			headers={'authorization': auth_header, 'content-type': 'application/json'}, data=data)
314
		
315
		sys.exit(0)
316
	
317

    
318

    
319
if 'e-mail' in options and 'stretched_password' in options:
320
	auth_header = hawk_header('{}', 'POST', '/v1/session/destroy', 'api.accounts.firefox.com', '443')
321
	r = req('post', 'https://api.accounts.firefox.com/v1/session/destroy',
322
		headers={'authorization': auth_header, 'content-type': 'application/json'}, data='{}')
    (1-1/1)