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