]> jfr.im git - yt-dlp.git/blame - yt_dlp/cookies.py
[CGTN] Add extractor (#981)
[yt-dlp.git] / yt_dlp / cookies.py
CommitLineData
982ee69a
MB
1import ctypes
2import json
3import os
4import shutil
982ee69a
MB
5import struct
6import subprocess
7import sys
8import tempfile
9from datetime import datetime, timedelta, timezone
10from hashlib import pbkdf2_hmac
11
12from yt_dlp.aes import aes_cbc_decrypt
13from yt_dlp.compat import (
14 compat_b64decode,
15 compat_cookiejar_Cookie,
edf65256 16 compat_pycrypto_AES
982ee69a
MB
17)
18from yt_dlp.utils import (
063c409d 19 bug_reports_message,
982ee69a
MB
20 bytes_to_intlist,
21 expand_path,
22 intlist_to_bytes,
23 process_communicate_or_kill,
24 YoutubeDLCookieJar,
25)
26
767b02a9
MB
27try:
28 import sqlite3
29 SQLITE_AVAILABLE = True
30except ImportError:
31 # although sqlite3 is part of the standard library, it is possible to compile python without
32 # sqlite support. See: https://github.com/yt-dlp/yt-dlp/issues/544
33 SQLITE_AVAILABLE = False
34
35
982ee69a
MB
36try:
37 import keyring
38 KEYRING_AVAILABLE = True
063c409d 39 KEYRING_UNAVAILABLE_REASON = f'due to unknown reasons{bug_reports_message()}'
982ee69a
MB
40except ImportError:
41 KEYRING_AVAILABLE = False
063c409d 42 KEYRING_UNAVAILABLE_REASON = (
43 'as the `keyring` module is not installed. '
44 'Please install by running `python3 -m pip install keyring`. '
45 'Depending on your platform, additional packages may be required '
46 'to access the keyring; see https://pypi.org/project/keyring')
47except Exception as _err:
48 KEYRING_AVAILABLE = False
49 KEYRING_UNAVAILABLE_REASON = 'as the `keyring` module could not be initialized: %s' % _err
982ee69a
MB
50
51
52CHROMIUM_BASED_BROWSERS = {'brave', 'chrome', 'chromium', 'edge', 'opera', 'vivaldi'}
53SUPPORTED_BROWSERS = CHROMIUM_BASED_BROWSERS | {'firefox', 'safari'}
54
55
56class YDLLogger:
57 def __init__(self, ydl=None):
58 self._ydl = ydl
59
60 def debug(self, message):
61 if self._ydl:
62 self._ydl.write_debug(message)
63
64 def info(self, message):
65 if self._ydl:
66 self._ydl.to_screen(f'[Cookies] {message}')
67
68 def warning(self, message, only_once=False):
69 if self._ydl:
70 self._ydl.report_warning(message, only_once)
71
72 def error(self, message):
73 if self._ydl:
74 self._ydl.report_error(message)
75
76
77def load_cookies(cookie_file, browser_specification, ydl):
78 cookie_jars = []
79 if browser_specification is not None:
80 browser_name, profile = _parse_browser_specification(*browser_specification)
81 cookie_jars.append(extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl)))
82
83 if cookie_file is not None:
84 cookie_file = expand_path(cookie_file)
85 jar = YoutubeDLCookieJar(cookie_file)
86 if os.access(cookie_file, os.R_OK):
87 jar.load(ignore_discard=True, ignore_expires=True)
88 cookie_jars.append(jar)
89
90 return _merge_cookie_jars(cookie_jars)
91
92
93def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger()):
94 if browser_name == 'firefox':
95 return _extract_firefox_cookies(profile, logger)
96 elif browser_name == 'safari':
97 return _extract_safari_cookies(profile, logger)
98 elif browser_name in CHROMIUM_BASED_BROWSERS:
99 return _extract_chrome_cookies(browser_name, profile, logger)
100 else:
101 raise ValueError('unknown browser: {}'.format(browser_name))
102
103
104def _extract_firefox_cookies(profile, logger):
105 logger.info('Extracting cookies from firefox')
767b02a9
MB
106 if not SQLITE_AVAILABLE:
107 logger.warning('Cannot extract cookies from firefox without sqlite3 support. '
108 'Please use a python interpreter compiled with sqlite3 support')
109 return YoutubeDLCookieJar()
982ee69a
MB
110
111 if profile is None:
112 search_root = _firefox_browser_dir()
113 elif _is_path(profile):
114 search_root = profile
115 else:
116 search_root = os.path.join(_firefox_browser_dir(), profile)
117
118 cookie_database_path = _find_most_recently_used_file(search_root, 'cookies.sqlite')
119 if cookie_database_path is None:
120 raise FileNotFoundError('could not find firefox cookies database in {}'.format(search_root))
526d74ec 121 logger.debug('Extracting cookies from: "{}"'.format(cookie_database_path))
982ee69a
MB
122
123 with tempfile.TemporaryDirectory(prefix='youtube_dl') as tmpdir:
124 cursor = None
125 try:
126 cursor = _open_database_copy(cookie_database_path, tmpdir)
127 cursor.execute('SELECT host, name, value, path, expiry, isSecure FROM moz_cookies')
128 jar = YoutubeDLCookieJar()
129 for host, name, value, path, expiry, is_secure in cursor.fetchall():
130 cookie = compat_cookiejar_Cookie(
131 version=0, name=name, value=value, port=None, port_specified=False,
132 domain=host, domain_specified=bool(host), domain_initial_dot=host.startswith('.'),
133 path=path, path_specified=bool(path), secure=is_secure, expires=expiry, discard=False,
134 comment=None, comment_url=None, rest={})
135 jar.set_cookie(cookie)
136 logger.info('Extracted {} cookies from firefox'.format(len(jar)))
137 return jar
138 finally:
139 if cursor is not None:
140 cursor.connection.close()
141
142
143def _firefox_browser_dir():
144 if sys.platform in ('linux', 'linux2'):
145 return os.path.expanduser('~/.mozilla/firefox')
146 elif sys.platform == 'win32':
147 return os.path.expandvars(r'%APPDATA%\Mozilla\Firefox\Profiles')
148 elif sys.platform == 'darwin':
149 return os.path.expanduser('~/Library/Application Support/Firefox')
150 else:
151 raise ValueError('unsupported platform: {}'.format(sys.platform))
152
153
154def _get_chromium_based_browser_settings(browser_name):
155 # https://chromium.googlesource.com/chromium/src/+/HEAD/docs/user_data_dir.md
156 if sys.platform in ('linux', 'linux2'):
157 config = _config_home()
158 browser_dir = {
159 'brave': os.path.join(config, 'BraveSoftware/Brave-Browser'),
160 'chrome': os.path.join(config, 'google-chrome'),
161 'chromium': os.path.join(config, 'chromium'),
162 'edge': os.path.join(config, 'microsoft-edge'),
163 'opera': os.path.join(config, 'opera'),
164 'vivaldi': os.path.join(config, 'vivaldi'),
165 }[browser_name]
166
167 elif sys.platform == 'win32':
168 appdata_local = os.path.expandvars('%LOCALAPPDATA%')
169 appdata_roaming = os.path.expandvars('%APPDATA%')
170 browser_dir = {
171 'brave': os.path.join(appdata_local, r'BraveSoftware\Brave-Browser\User Data'),
172 'chrome': os.path.join(appdata_local, r'Google\Chrome\User Data'),
173 'chromium': os.path.join(appdata_local, r'Chromium\User Data'),
174 'edge': os.path.join(appdata_local, r'Microsoft\Edge\User Data'),
175 'opera': os.path.join(appdata_roaming, r'Opera Software\Opera Stable'),
176 'vivaldi': os.path.join(appdata_local, r'Vivaldi\User Data'),
177 }[browser_name]
178
179 elif sys.platform == 'darwin':
180 appdata = os.path.expanduser('~/Library/Application Support')
181 browser_dir = {
182 'brave': os.path.join(appdata, 'BraveSoftware/Brave-Browser'),
183 'chrome': os.path.join(appdata, 'Google/Chrome'),
184 'chromium': os.path.join(appdata, 'Chromium'),
185 'edge': os.path.join(appdata, 'Microsoft Edge'),
186 'opera': os.path.join(appdata, 'com.operasoftware.Opera'),
187 'vivaldi': os.path.join(appdata, 'Vivaldi'),
188 }[browser_name]
189
190 else:
191 raise ValueError('unsupported platform: {}'.format(sys.platform))
192
193 # Linux keyring names can be determined by snooping on dbus while opening the browser in KDE:
194 # dbus-monitor "interface='org.kde.KWallet'" "type=method_return"
195 keyring_name = {
196 'brave': 'Brave',
197 'chrome': 'Chrome',
198 'chromium': 'Chromium',
29b208f6 199 'edge': 'Microsoft Edge' if sys.platform == 'darwin' else 'Chromium',
982ee69a
MB
200 'opera': 'Opera' if sys.platform == 'darwin' else 'Chromium',
201 'vivaldi': 'Vivaldi' if sys.platform == 'darwin' else 'Chrome',
202 }[browser_name]
203
204 browsers_without_profiles = {'opera'}
205
206 return {
207 'browser_dir': browser_dir,
208 'keyring_name': keyring_name,
209 'supports_profiles': browser_name not in browsers_without_profiles
210 }
211
212
213def _extract_chrome_cookies(browser_name, profile, logger):
214 logger.info('Extracting cookies from {}'.format(browser_name))
767b02a9
MB
215
216 if not SQLITE_AVAILABLE:
217 logger.warning(('Cannot extract cookies from {} without sqlite3 support. '
218 'Please use a python interpreter compiled with sqlite3 support').format(browser_name))
219 return YoutubeDLCookieJar()
220
982ee69a
MB
221 config = _get_chromium_based_browser_settings(browser_name)
222
223 if profile is None:
224 search_root = config['browser_dir']
225 elif _is_path(profile):
226 search_root = profile
227 config['browser_dir'] = os.path.dirname(profile) if config['supports_profiles'] else profile
228 else:
229 if config['supports_profiles']:
230 search_root = os.path.join(config['browser_dir'], profile)
231 else:
232 logger.error('{} does not support profiles'.format(browser_name))
233 search_root = config['browser_dir']
234
235 cookie_database_path = _find_most_recently_used_file(search_root, 'Cookies')
236 if cookie_database_path is None:
237 raise FileNotFoundError('could not find {} cookies database in "{}"'.format(browser_name, search_root))
526d74ec 238 logger.debug('Extracting cookies from: "{}"'.format(cookie_database_path))
982ee69a
MB
239
240 decryptor = get_cookie_decryptor(config['browser_dir'], config['keyring_name'], logger)
241
242 with tempfile.TemporaryDirectory(prefix='youtube_dl') as tmpdir:
243 cursor = None
244 try:
245 cursor = _open_database_copy(cookie_database_path, tmpdir)
246 cursor.connection.text_factory = bytes
247 column_names = _get_column_names(cursor, 'cookies')
248 secure_column = 'is_secure' if 'is_secure' in column_names else 'secure'
249 cursor.execute('SELECT host_key, name, value, encrypted_value, path, '
250 'expires_utc, {} FROM cookies'.format(secure_column))
251 jar = YoutubeDLCookieJar()
252 failed_cookies = 0
253 for host_key, name, value, encrypted_value, path, expires_utc, is_secure in cursor.fetchall():
254 host_key = host_key.decode('utf-8')
255 name = name.decode('utf-8')
256 value = value.decode('utf-8')
257 path = path.decode('utf-8')
258
259 if not value and encrypted_value:
260 value = decryptor.decrypt(encrypted_value)
261 if value is None:
262 failed_cookies += 1
263 continue
264
265 cookie = compat_cookiejar_Cookie(
266 version=0, name=name, value=value, port=None, port_specified=False,
267 domain=host_key, domain_specified=bool(host_key), domain_initial_dot=host_key.startswith('.'),
268 path=path, path_specified=bool(path), secure=is_secure, expires=expires_utc, discard=False,
269 comment=None, comment_url=None, rest={})
270 jar.set_cookie(cookie)
271 if failed_cookies > 0:
272 failed_message = ' ({} could not be decrypted)'.format(failed_cookies)
273 else:
274 failed_message = ''
275 logger.info('Extracted {} cookies from {}{}'.format(len(jar), browser_name, failed_message))
276 return jar
277 finally:
278 if cursor is not None:
279 cursor.connection.close()
280
281
282class ChromeCookieDecryptor:
283 """
284 Overview:
285
286 Linux:
287 - cookies are either v10 or v11
288 - v10: AES-CBC encrypted with a fixed key
289 - v11: AES-CBC encrypted with an OS protected key (keyring)
290 - v11 keys can be stored in various places depending on the activate desktop environment [2]
291
292 Mac:
293 - cookies are either v10 or not v10
294 - v10: AES-CBC encrypted with an OS protected key (keyring) and more key derivation iterations than linux
295 - not v10: 'old data' stored as plaintext
296
297 Windows:
298 - cookies are either v10 or not v10
299 - v10: AES-GCM encrypted with a key which is encrypted with DPAPI
300 - not v10: encrypted with DPAPI
301
302 Sources:
303 - [1] https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/
304 - [2] https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/key_storage_linux.cc
305 - KeyStorageLinux::CreateService
306 """
307
308 def decrypt(self, encrypted_value):
309 raise NotImplementedError
310
311
312def get_cookie_decryptor(browser_root, browser_keyring_name, logger):
313 if sys.platform in ('linux', 'linux2'):
314 return LinuxChromeCookieDecryptor(browser_keyring_name, logger)
315 elif sys.platform == 'darwin':
316 return MacChromeCookieDecryptor(browser_keyring_name, logger)
317 elif sys.platform == 'win32':
318 return WindowsChromeCookieDecryptor(browser_root, logger)
319 else:
320 raise NotImplementedError('Chrome cookie decryption is not supported '
321 'on this platform: {}'.format(sys.platform))
322
323
324class LinuxChromeCookieDecryptor(ChromeCookieDecryptor):
325 def __init__(self, browser_keyring_name, logger):
326 self._logger = logger
327 self._v10_key = self.derive_key(b'peanuts')
328 if KEYRING_AVAILABLE:
329 self._v11_key = self.derive_key(_get_linux_keyring_password(browser_keyring_name))
330 else:
331 self._v11_key = None
332
333 @staticmethod
334 def derive_key(password):
335 # values from
336 # https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_linux.cc
337 return pbkdf2_sha1(password, salt=b'saltysalt', iterations=1, key_length=16)
338
339 def decrypt(self, encrypted_value):
340 version = encrypted_value[:3]
341 ciphertext = encrypted_value[3:]
342
343 if version == b'v10':
344 return _decrypt_aes_cbc(ciphertext, self._v10_key, self._logger)
345
346 elif version == b'v11':
347 if self._v11_key is None:
063c409d 348 self._logger.warning(f'cannot decrypt cookie {KEYRING_UNAVAILABLE_REASON}', only_once=True)
982ee69a
MB
349 return None
350 return _decrypt_aes_cbc(ciphertext, self._v11_key, self._logger)
351
352 else:
353 return None
354
355
356class MacChromeCookieDecryptor(ChromeCookieDecryptor):
357 def __init__(self, browser_keyring_name, logger):
358 self._logger = logger
359 password = _get_mac_keyring_password(browser_keyring_name)
360 self._v10_key = None if password is None else self.derive_key(password)
361
362 @staticmethod
363 def derive_key(password):
364 # values from
365 # https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_mac.mm
366 return pbkdf2_sha1(password, salt=b'saltysalt', iterations=1003, key_length=16)
367
368 def decrypt(self, encrypted_value):
369 version = encrypted_value[:3]
370 ciphertext = encrypted_value[3:]
371
372 if version == b'v10':
373 if self._v10_key is None:
374 self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True)
375 return None
376
377 return _decrypt_aes_cbc(ciphertext, self._v10_key, self._logger)
378
379 else:
380 # other prefixes are considered 'old data' which were stored as plaintext
381 # https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_mac.mm
382 return encrypted_value
383
384
385class WindowsChromeCookieDecryptor(ChromeCookieDecryptor):
386 def __init__(self, browser_root, logger):
387 self._logger = logger
388 self._v10_key = _get_windows_v10_key(browser_root, logger)
389
390 def decrypt(self, encrypted_value):
391 version = encrypted_value[:3]
392 ciphertext = encrypted_value[3:]
393
394 if version == b'v10':
395 if self._v10_key is None:
396 self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True)
397 return None
edf65256 398 elif not compat_pycrypto_AES:
982ee69a
MB
399 self._logger.warning('cannot decrypt cookie as the `pycryptodome` module is not installed. '
400 'Please install by running `python3 -m pip install pycryptodome`',
401 only_once=True)
402 return None
403
404 # https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_win.cc
405 # kNonceLength
406 nonce_length = 96 // 8
407 # boringssl
408 # EVP_AEAD_AES_GCM_TAG_LEN
409 authentication_tag_length = 16
410
411 raw_ciphertext = ciphertext
412 nonce = raw_ciphertext[:nonce_length]
413 ciphertext = raw_ciphertext[nonce_length:-authentication_tag_length]
414 authentication_tag = raw_ciphertext[-authentication_tag_length:]
415
416 return _decrypt_aes_gcm(ciphertext, self._v10_key, nonce, authentication_tag, self._logger)
417
418 else:
419 # any other prefix means the data is DPAPI encrypted
420 # https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_win.cc
421 return _decrypt_windows_dpapi(encrypted_value, self._logger).decode('utf-8')
422
423
424def _extract_safari_cookies(profile, logger):
425 if profile is not None:
426 logger.error('safari does not support profiles')
427 if sys.platform != 'darwin':
428 raise ValueError('unsupported platform: {}'.format(sys.platform))
429
430 cookies_path = os.path.expanduser('~/Library/Cookies/Cookies.binarycookies')
431
432 if not os.path.isfile(cookies_path):
433 raise FileNotFoundError('could not find safari cookies database')
434
435 with open(cookies_path, 'rb') as f:
436 cookies_data = f.read()
437
438 jar = parse_safari_cookies(cookies_data, logger=logger)
439 logger.info('Extracted {} cookies from safari'.format(len(jar)))
440 return jar
441
442
443class ParserError(Exception):
444 pass
445
446
447class DataParser:
448 def __init__(self, data, logger):
449 self._data = data
450 self.cursor = 0
451 self._logger = logger
452
453 def read_bytes(self, num_bytes):
454 if num_bytes < 0:
455 raise ParserError('invalid read of {} bytes'.format(num_bytes))
456 end = self.cursor + num_bytes
457 if end > len(self._data):
458 raise ParserError('reached end of input')
459 data = self._data[self.cursor:end]
460 self.cursor = end
461 return data
462
463 def expect_bytes(self, expected_value, message):
464 value = self.read_bytes(len(expected_value))
465 if value != expected_value:
466 raise ParserError('unexpected value: {} != {} ({})'.format(value, expected_value, message))
467
468 def read_uint(self, big_endian=False):
469 data_format = '>I' if big_endian else '<I'
470 return struct.unpack(data_format, self.read_bytes(4))[0]
471
472 def read_double(self, big_endian=False):
473 data_format = '>d' if big_endian else '<d'
474 return struct.unpack(data_format, self.read_bytes(8))[0]
475
476 def read_cstring(self):
477 buffer = []
478 while True:
479 c = self.read_bytes(1)
480 if c == b'\x00':
481 return b''.join(buffer).decode('utf-8')
482 else:
483 buffer.append(c)
484
485 def skip(self, num_bytes, description='unknown'):
486 if num_bytes > 0:
487 self._logger.debug('skipping {} bytes ({}): {}'.format(
488 num_bytes, description, self.read_bytes(num_bytes)))
489 elif num_bytes < 0:
490 raise ParserError('invalid skip of {} bytes'.format(num_bytes))
491
492 def skip_to(self, offset, description='unknown'):
493 self.skip(offset - self.cursor, description)
494
495 def skip_to_end(self, description='unknown'):
496 self.skip_to(len(self._data), description)
497
498
499def _mac_absolute_time_to_posix(timestamp):
500 return int((datetime(2001, 1, 1, 0, 0, tzinfo=timezone.utc) + timedelta(seconds=timestamp)).timestamp())
501
502
503def _parse_safari_cookies_header(data, logger):
504 p = DataParser(data, logger)
505 p.expect_bytes(b'cook', 'database signature')
506 number_of_pages = p.read_uint(big_endian=True)
507 page_sizes = [p.read_uint(big_endian=True) for _ in range(number_of_pages)]
508 return page_sizes, p.cursor
509
510
511def _parse_safari_cookies_page(data, jar, logger):
512 p = DataParser(data, logger)
513 p.expect_bytes(b'\x00\x00\x01\x00', 'page signature')
514 number_of_cookies = p.read_uint()
515 record_offsets = [p.read_uint() for _ in range(number_of_cookies)]
516 if number_of_cookies == 0:
517 logger.debug('a cookies page of size {} has no cookies'.format(len(data)))
518 return
519
520 p.skip_to(record_offsets[0], 'unknown page header field')
521
522 for record_offset in record_offsets:
523 p.skip_to(record_offset, 'space between records')
524 record_length = _parse_safari_cookies_record(data[record_offset:], jar, logger)
525 p.read_bytes(record_length)
526 p.skip_to_end('space in between pages')
527
528
529def _parse_safari_cookies_record(data, jar, logger):
530 p = DataParser(data, logger)
531 record_size = p.read_uint()
532 p.skip(4, 'unknown record field 1')
533 flags = p.read_uint()
534 is_secure = bool(flags & 0x0001)
535 p.skip(4, 'unknown record field 2')
536 domain_offset = p.read_uint()
537 name_offset = p.read_uint()
538 path_offset = p.read_uint()
539 value_offset = p.read_uint()
540 p.skip(8, 'unknown record field 3')
541 expiration_date = _mac_absolute_time_to_posix(p.read_double())
542 _creation_date = _mac_absolute_time_to_posix(p.read_double()) # noqa: F841
543
544 try:
545 p.skip_to(domain_offset)
546 domain = p.read_cstring()
547
548 p.skip_to(name_offset)
549 name = p.read_cstring()
550
551 p.skip_to(path_offset)
552 path = p.read_cstring()
553
554 p.skip_to(value_offset)
555 value = p.read_cstring()
556 except UnicodeDecodeError:
f9be9cb9 557 logger.warning('failed to parse cookie because UTF-8 decoding failed', only_once=True)
982ee69a
MB
558 return record_size
559
560 p.skip_to(record_size, 'space at the end of the record')
561
562 cookie = compat_cookiejar_Cookie(
563 version=0, name=name, value=value, port=None, port_specified=False,
564 domain=domain, domain_specified=bool(domain), domain_initial_dot=domain.startswith('.'),
565 path=path, path_specified=bool(path), secure=is_secure, expires=expiration_date, discard=False,
566 comment=None, comment_url=None, rest={})
567 jar.set_cookie(cookie)
568 return record_size
569
570
571def parse_safari_cookies(data, jar=None, logger=YDLLogger()):
572 """
573 References:
574 - https://github.com/libyal/dtformats/blob/main/documentation/Safari%20Cookies.asciidoc
575 - this data appears to be out of date but the important parts of the database structure is the same
576 - there are a few bytes here and there which are skipped during parsing
577 """
578 if jar is None:
579 jar = YoutubeDLCookieJar()
580 page_sizes, body_start = _parse_safari_cookies_header(data, logger)
581 p = DataParser(data[body_start:], logger)
582 for page_size in page_sizes:
583 _parse_safari_cookies_page(p.read_bytes(page_size), jar, logger)
584 p.skip_to_end('footer')
585 return jar
586
587
588def _get_linux_keyring_password(browser_keyring_name):
589 password = keyring.get_password('{} Keys'.format(browser_keyring_name),
590 '{} Safe Storage'.format(browser_keyring_name))
591 if password is None:
592 # this sometimes occurs in KDE because chrome does not check hasEntry and instead
593 # just tries to read the value (which kwallet returns "") whereas keyring checks hasEntry
594 # to verify this:
595 # dbus-monitor "interface='org.kde.KWallet'" "type=method_return"
596 # while starting chrome.
597 # this may be a bug as the intended behaviour is to generate a random password and store
598 # it, but that doesn't matter here.
599 password = ''
600 return password.encode('utf-8')
601
602
603def _get_mac_keyring_password(browser_keyring_name):
604 if KEYRING_AVAILABLE:
605 password = keyring.get_password('{} Safe Storage'.format(browser_keyring_name), browser_keyring_name)
606 return password.encode('utf-8')
607 else:
608 proc = subprocess.Popen(['security', 'find-generic-password',
609 '-w', # write password to stdout
610 '-a', browser_keyring_name, # match 'account'
611 '-s', '{} Safe Storage'.format(browser_keyring_name)], # match 'service'
612 stdout=subprocess.PIPE,
613 stderr=subprocess.DEVNULL)
614 try:
615 stdout, stderr = process_communicate_or_kill(proc)
616 return stdout
617 except BaseException:
618 return None
619
620
621def _get_windows_v10_key(browser_root, logger):
622 path = _find_most_recently_used_file(browser_root, 'Local State')
623 if path is None:
624 logger.error('could not find local state file')
625 return None
626 with open(path, 'r') as f:
627 data = json.load(f)
628 try:
629 base64_key = data['os_crypt']['encrypted_key']
630 except KeyError:
631 logger.error('no encrypted key in Local State')
632 return None
633 encrypted_key = compat_b64decode(base64_key)
634 prefix = b'DPAPI'
635 if not encrypted_key.startswith(prefix):
636 logger.error('invalid key')
637 return None
638 return _decrypt_windows_dpapi(encrypted_key[len(prefix):], logger)
639
640
641def pbkdf2_sha1(password, salt, iterations, key_length):
642 return pbkdf2_hmac('sha1', password, salt, iterations, key_length)
643
644
645def _decrypt_aes_cbc(ciphertext, key, logger, initialization_vector=b' ' * 16):
646 plaintext = aes_cbc_decrypt(bytes_to_intlist(ciphertext),
647 bytes_to_intlist(key),
648 bytes_to_intlist(initialization_vector))
649 padding_length = plaintext[-1]
650 try:
651 return intlist_to_bytes(plaintext[:-padding_length]).decode('utf-8')
652 except UnicodeDecodeError:
f9be9cb9 653 logger.warning('failed to decrypt cookie because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
982ee69a
MB
654 return None
655
656
657def _decrypt_aes_gcm(ciphertext, key, nonce, authentication_tag, logger):
edf65256 658 cipher = compat_pycrypto_AES.new(key, compat_pycrypto_AES.MODE_GCM, nonce)
982ee69a
MB
659 try:
660 plaintext = cipher.decrypt_and_verify(ciphertext, authentication_tag)
661 except ValueError:
f9be9cb9 662 logger.warning('failed to decrypt cookie because the MAC check failed. Possibly the key is wrong?', only_once=True)
982ee69a
MB
663 return None
664
665 try:
666 return plaintext.decode('utf-8')
667 except UnicodeDecodeError:
f9be9cb9 668 logger.warning('failed to decrypt cookie because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
982ee69a
MB
669 return None
670
671
672def _decrypt_windows_dpapi(ciphertext, logger):
673 """
674 References:
675 - https://docs.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptunprotectdata
676 """
677 from ctypes.wintypes import DWORD
678
679 class DATA_BLOB(ctypes.Structure):
680 _fields_ = [('cbData', DWORD),
681 ('pbData', ctypes.POINTER(ctypes.c_char))]
682
683 buffer = ctypes.create_string_buffer(ciphertext)
684 blob_in = DATA_BLOB(ctypes.sizeof(buffer), buffer)
685 blob_out = DATA_BLOB()
686 ret = ctypes.windll.crypt32.CryptUnprotectData(
687 ctypes.byref(blob_in), # pDataIn
688 None, # ppszDataDescr: human readable description of pDataIn
689 None, # pOptionalEntropy: salt?
690 None, # pvReserved: must be NULL
691 None, # pPromptStruct: information about prompts to display
692 0, # dwFlags
693 ctypes.byref(blob_out) # pDataOut
694 )
695 if not ret:
f9be9cb9 696 logger.warning('failed to decrypt with DPAPI', only_once=True)
982ee69a
MB
697 return None
698
699 result = ctypes.string_at(blob_out.pbData, blob_out.cbData)
700 ctypes.windll.kernel32.LocalFree(blob_out.pbData)
701 return result
702
703
704def _config_home():
705 return os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
706
707
708def _open_database_copy(database_path, tmpdir):
709 # cannot open sqlite databases if they are already in use (e.g. by the browser)
710 database_copy_path = os.path.join(tmpdir, 'temporary.sqlite')
711 shutil.copy(database_path, database_copy_path)
712 conn = sqlite3.connect(database_copy_path)
713 return conn.cursor()
714
715
716def _get_column_names(cursor, table_name):
717 table_info = cursor.execute('PRAGMA table_info({})'.format(table_name)).fetchall()
718 return [row[1].decode('utf-8') for row in table_info]
719
720
721def _find_most_recently_used_file(root, filename):
722 # if there are multiple browser profiles, take the most recently used one
723 paths = []
724 for root, dirs, files in os.walk(root):
725 for file in files:
726 if file == filename:
727 paths.append(os.path.join(root, file))
728 return None if not paths else max(paths, key=lambda path: os.lstat(path).st_mtime)
729
730
731def _merge_cookie_jars(jars):
732 output_jar = YoutubeDLCookieJar()
733 for jar in jars:
734 for cookie in jar:
735 output_jar.set_cookie(cookie)
736 if jar.filename is not None:
737 output_jar.filename = jar.filename
738 return output_jar
739
740
741def _is_path(value):
742 return os.path.sep in value
743
744
745def _parse_browser_specification(browser_name, profile=None):
ca46b941 746 browser_name = browser_name.lower()
982ee69a
MB
747 if browser_name not in SUPPORTED_BROWSERS:
748 raise ValueError(f'unsupported browser: "{browser_name}"')
749 if profile is not None and _is_path(profile):
750 profile = os.path.expanduser(profile)
751 return browser_name, profile