File Coverage

blib/lib/HTTP/Cookies/Chrome.pm
Criterion Covered Total %
statement 192 220 87.2
branch 27 64 42.1
condition 17 43 39.5
subroutine 41 44 93.1
pod 6 6 100.0
total 283 377 75.0


line stmt bran cond sub pod time code
1 4     4   70491 use 5.010;
  4         21  
2 4     4   2639 use utf8;
  4         59  
  4         23  
3              
4             package HTTP::Cookies::Chrome;
5 4     4   204 use strict;
  4         9  
  4         83  
6              
7 4     4   19 use warnings;
  4         6  
  4         110  
8 4     4   20 use warnings::register;
  4         6  
  4         450  
9              
10 4     4   2284 use POSIX;
  4         27134  
  4         23  
11              
12             BEGIN {
13 4     4   24 my @names = qw( _VERSION KEY VALUE PATH DOMAIN PORT PATH_SPEC
14             SECURE EXPIRES DISCARD REST );
15 4         9 my $n = 0;
16 4         11 foreach my $name ( @names ) {
17 4     4   11991 no strict 'refs';
  4         10  
  4         370  
18 44         62 my $m = $n++;
19 44         422 *{$name} = sub () { $m }
  0         0  
20 44         245 }
21             }
22              
23             =encoding utf8
24              
25             =head1 NAME
26              
27             HTTP::Cookies::Chrome - Cookie storage and management for Google Chrome
28              
29             =head1 SYNOPSIS
30              
31             use HTTP::Cookies::Chrome;
32              
33             my $password = HTTP::Cookies::Chrome->get_from_gnome;
34              
35             my $cookie_jar = HTTP::Cookies::Chrome->new(
36             chrome_safe_storage_password => $password,
37             file => ...,
38             autosave => ...,
39             );
40             $cookie_jar->load( $path_to_cookies );
41              
42             # otherwise same as HTTP::Cookies
43              
44             =head1 DESCRIPTION
45              
46             This package overrides the C and C methods of
47             C so it can work with Google Chrome cookie files,
48             which are SQLite databases. This also should work from Chrome clones,
49             such as Brave.
50              
51             First, you are allowed to create different profiles within Chrome, and
52             each profile has its own set of files. The default profile is just C.
53             Along with that, there are various clones with their own product names.
54             The expected paths incorporate the product and profiles:
55              
56             Starting with Chrome 80, cookie values may be (likely are) encrypted
57             with a password that Chrome changes and stores somewhere. Additionally,
58             each cookie record tracks several other fields. If you are
59             using an earlier Chrome, you should use an older version of this module
60             (the 1.x series).
61              
62             =over 4
63              
64             =item macOS - ~/Library/Application Support/PRODUCT/Chrome/PROFILE/Cookies
65              
66             =item Linux - ~/.config/PRODUCT/PROFILE/Cookies
67              
68             =item Windows - C:\Users\USER\AppData\Local\PRODUCT\User Data\$profile\Cookies
69              
70             =back
71              
72             =cut
73              
74 4     4   30 use base qw( HTTP::Cookies );
  4         7  
  4         2666  
75 4     4   51963 use vars qw( $VERSION );
  4         11  
  4         197  
76              
77 4     4   25 use constant TRUE => 1;
  4         10  
  4         287  
78 4     4   61 use constant FALSE => 0;
  4         22  
  4         210  
79              
80             $VERSION = '2.002';
81              
82 4     4   6673 use DBI;
  4         72603  
  4         4837  
83              
84              
85             sub _add_value {
86 8     8   27 my( $self, $key, $value ) = @_;
87 8         29 $self->_stash->{$key} = $value;
88             }
89              
90 437     437   901 sub _cipher { $_[0]->_get_value( 'cipher' ) }
91              
92             sub _connect {
93 5     5   20 my( $self, $file ) = @_;
94 5         69 my $dbh = DBI->connect( "dbi:SQLite:dbname=$file", '', '',
95             {
96             sqlite_see_if_its_a_number => 1,
97             } );
98 5         38336 $_[0]->{dbh} = $dbh;
99             }
100              
101             sub _create_table {
102 1     1   4 my( $self ) = @_;
103              
104 1         4 $self->_dbh->do( 'DROP TABLE IF EXISTS cookies' );
105              
106 1         364 $self->_dbh->do( <<'SQL' );
107             CREATE TABLE cookies(
108             creation_utc INTEGER NOT NULL,
109             host_key TEXT NOT NULL,
110             name TEXT NOT NULL,
111             value TEXT NOT NULL,
112             path TEXT NOT NULL,
113             expires_utc INTEGER NOT NULL,
114             is_secure INTEGER NOT NULL,
115             is_httponly INTEGER NOT NULL,
116             last_access_utc INTEGER NOT NULL,
117             has_expires INTEGER NOT NULL DEFAULT 1,
118             is_persistent INTEGER NOT NULL DEFAULT 1,
119             priority INTEGER NOT NULL DEFAULT 1,
120             encrypted_value BLOB DEFAULT '',
121             samesite INTEGER NOT NULL DEFAULT -1,
122             source_scheme INTEGER NOT NULL DEFAULT 0,
123             source_port INTEGER NOT NULL DEFAULT -1,
124             is_same_party INTEGER NOT NULL DEFAULT 0,
125             UNIQUE (host_key, name, path)
126             )
127             SQL
128             }
129              
130 3     3   51 sub _dbh { $_[0]->{dbh} }
131              
132             sub _decrypt {
133 184     184   350 my( $self, $blob ) = @_;
134              
135 184 50       351 unless( $self->_cipher ) {
136 0 0       0 warnings::warn("Decrypted cookies is not set up") if warnings::enabled();
137 0         0 return;
138             }
139              
140 184         371 my $type = substr $blob, 0, 3;
141 184 50       394 unless( $type eq 'v10' ) { # v11 is a thing, too
142 0 0       0 warnings::warn("Encrypted value is unexpected type <$type>") if warnings::enabled();
143 0         0 return;
144             }
145              
146 184         347 my $plaintext = $self->_cipher->decrypt( substr $blob, 3 );
147 184         470 my $padding_count = ord( substr $plaintext, -1 );
148 184 50       410 substr( $plaintext, -$padding_count ) = '' if $padding_count < 16;
149              
150 184         437 $plaintext;
151             }
152              
153             sub _encrypt {
154 23     23   56 my( $self, $value ) = @_;
155              
156 23 50       75 unless( defined $value ) {
157 0 0       0 warnings::warn("Value is not defined! Nothing to encrypt!") if warnings::enabled();
158 0         0 return;
159             }
160              
161 23 50       61 unless( $self->_cipher ) {
162 0 0       0 warnings::warn("Encrypted cookies is not set up") if warnings::enabled();
163 0         0 return;
164             }
165              
166 23         59 my $blocksize = 16;
167              
168 23         77 my $padding_length = ($blocksize - length($value) % $blocksize);
169 23         107 my $padding = chr($padding_length) x $padding_length;
170 23         62 my $encrypted = 'v10' . $self->_cipher->encrypt( $value . $padding );
171              
172 23         108 $encrypted;
173             }
174              
175             sub _filter_cookies {
176 1     1   4 my( $self ) = @_;
177              
178             $self->scan(
179             sub {
180 23     23   960 my( $version, $key, $val, $path, $domain, $port,
181             $path_spec, $secure, $expires, $discard, $rest ) = @_;
182              
183 23         111 my @parts = @_;
184              
185 23 50 33     124 return if $parts[DISCARD] && not $self->{ignore_discard};
186 23 50 33     204 return if defined $parts[EXPIRES] && time > $parts[EXPIRES];
187              
188 23         96 $parts[EXPIRES] = $rest->{expires_utc};
189 23 100       85 $parts[SECURE] = $parts[SECURE] ? TRUE : FALSE;
190              
191 23 100       183 my $bool = $domain =~ /^\./ ? TRUE : FALSE;
192              
193 23         138 $self->_insert( @parts );
194             }
195 1         37 );
196              
197             }
198              
199             sub _get_rows {
200 4     4   794 my( $self, $file ) = @_;
201              
202 4         22 my $dbh = $self->_connect( $file );
203              
204 4         37 my $sth = $dbh->prepare( 'SELECT * FROM cookies' );
205              
206 4         2274 $sth->execute;
207              
208             my @rows =
209             map {
210 92 50       453 if( my $e = $_->encrypted_value ) {
211 92         206 my $p = $self->_decrypt( $e );
212 92         231 $_->decrypted_value( $self->_decrypt( $e ) );
213             }
214 92         225 $_;
215             }
216 92         206 map { HTTP::Cookies::Chrome::Record->new( $_ ) }
217 4         23 @{ $sth->fetchall_arrayref };
  4         622  
218              
219 4         340 $dbh->disconnect;
220              
221 4         108 \@rows;
222             }
223              
224             sub _get_value {
225 437     437   938 my( $self, $key ) = @_;
226 437         978 $self->_stash->{$key}
227             }
228              
229             {
230             my $creation_offset = 0;
231              
232             sub _insert {
233 23     23   86 my( $self, @parts ) = @_;
234              
235 23         84 my $rest = $parts[REST];
236              
237 23   50     167 $rest->{httponly} //= 0;
238 23   50     103 $rest->{samesite} //= 0;
239              
240             # possibly thinking about a feature to remove the encryption,
241             # so we'd need to re-encrypt things. Here we assume that already
242             # exists so we always re-encrypt.
243 23         54 my $encrypted_value = '';
244              
245             # If we have a value and there was a previous encrypted value,
246             # encrypted the current value and blank out the value. Other
247 23 50 33     195 if( $parts[VALUE] and $rest->{encrypted_value} and $self->_cipher ) {
      33        
248 23         87 $encrypted_value = $self->_encrypt( $parts[VALUE] );
249 23         61 $parts[VALUE] = '';
250             }
251              
252             # Some cookies don't have values. WTF?
253 23   50     71 $parts[VALUE] //= '';
254              
255             my @values = (
256             $rest->{creation_utc},
257             @parts[DOMAIN, KEY, VALUE, PATH],
258             $rest->{expires_utc},
259             $parts[SECURE],
260 23         84 @{ $rest }{ qw(is_httponly last_access_utc has_expires
261             is_persistent priority) },
262             $encrypted_value,
263 23         97 @{ $rest }{ qw(samesite source_scheme ) },
264             $parts[PORT],
265             $rest->{is_same_party},
266 23         105 );
267              
268 23         379487 $self->{insert_sth}->execute( @values );
269             }
270             }
271              
272             sub _get_utc_microseconds {
273 4     4   43 no warnings 'uninitialized';
  4         9  
  4         246  
274 4     4   2831 use bignum;
  4         26003  
  4         28  
275 0   0 0   0 POSIX::strftime( '%s', gmtime() ) * 1_000_000 + ($_[1]//0);
276             }
277              
278             sub _make_cipher {
279 4     4   15 my( $self, $password ) = @_;
280              
281 4         14 my $key = do {
282 4         1566 state $rc2 = require PBKDF2::Tiny;
283 4         13827 my $s = _platform_settings();
284 4         14 my $salt = 'saltysalt';
285 4         10 my $length = 16;
286 4         26 PBKDF2::Tiny::derive( 'SHA-1', $password, $salt, $s->{iterations}, $length );
287             };
288              
289 4         1752 state $rc1 = require Crypt::Rijndael;
290 4         1375 my $cipher = Crypt::Rijndael->new( $key, Crypt::Rijndael::MODE_CBC() );
291 4         36 $cipher->set_iv( ' ' x 16 );
292              
293 4         26 $self->_add_value( chrome_safe_storage_password => $password );
294 4         15 $self->_add_value( cipher => $cipher );
295             }
296              
297             sub _platform_settings {
298             # https://n8henrie.com/2014/05/decrypt-chrome-cookies-with-python/
299             # https://github.com/n8henrie/pycookiecheat/issues/12
300 4     4   26 state $settings = {
301             darwin => {
302             iterations => 1003,
303             },
304             linux => {
305             iterations => 1,
306             },
307             MSWin32 => {
308             },
309             };
310              
311 4         24 $settings->{$^O};
312             }
313              
314             sub _prepare_insert {
315 1     1   7 my( $self ) = @_;
316              
317 1         7 my $sth = $self->{insert_sth} = $self->_dbh->prepare_cached( <<'SQL' );
318             INSERT INTO cookies VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
319             SQL
320              
321             }
322              
323             sub _stash {
324 445     445   694 state $mod_key = 'X-CHROME';
325 445   100     2848 $_[0]->{$mod_key} //= {};
326             }
327              
328             =head2 Class methods
329              
330             =over 4
331              
332             =item * guess_password
333              
334             Try to retrieve the Chrome Safe Storage password by accessing the
335             system secrets for the logged-in user. This returns nothing if it
336             can't find it.
337              
338             You don't need to use this to get the password.
339              
340             On macOS, this looks in the Keyring using C.
341              
342             On Linux, this uses C, which you might have to install
343             separately. Also, some early versions used the hard-coded password
344             C, and some others may have used C.
345              
346             I don't know how to do this on Windows. If you know, send a pull request.
347             That goes for other systems too.
348              
349             =cut
350              
351             sub guess_password {
352 0     0 1 0 my $p = do {
353 0 0       0 if( $^O eq 'darwin' ) { `security find-generic-password -a "Chrome" -w` }
  0 0       0  
354 0         0 elsif( $^O eq 'linux' ) { `secret-tool lookup xdg:schema chrome_libsecret_os_crypt_password application chrome` }
355             };
356 0         0 chomp $p;
357 0         0 $p
358             }
359              
360             =item * guess_path( PROFILE )
361              
362             Try to retrieve the directory that contains the Cookies file. If you
363             don't specify C, it uses C.
364              
365             macOS: F<~/Library/Application Support/Google/Chrome/PROFILE/Cookies>
366              
367             Linux: F<~/.config/google-chrome/PROFILE/Cookies>
368              
369             =cut
370              
371             sub guess_path {
372 0     0 1 0 my( $self, $profile ) = @_;
373 0   0     0 $profile //= 'Default';
374              
375 0         0 my $path_to_cookies = do {
376 0 0       0 if( $^O eq 'darwin' ) { "$ENV{HOME}/Library/Application Support/Google/Chrome/$profile/Cookies" }
  0 0       0  
377 0         0 elsif( $^O eq 'linux' ) { "$ENV{HOME}/.config/google-chrome/$profile/Cookies" }
378             };
379              
380 0 0       0 return unless -e $path_to_cookies;
381 0         0 $path_to_cookies
382             }
383              
384             =item * new
385              
386             The extends the C in L, with the additional parameter
387             for the decryption password.
388              
389             chrome_safe_storage_password - the password
390              
391             =cut
392              
393             sub new {
394 4     4 1 1752 my( $class, %args ) = @_;
395              
396 4         17 my $pass = delete $args{chrome_safe_storage_password};
397 4         17 my $file = delete $args{file};
398              
399 4         49 my $self = $class->SUPER::new( %args );
400              
401 4 50       32 return $self unless defined $pass;
402              
403 4         226 print STDERR "Making cipher\n";
404 4         46 $self->_make_cipher( $pass );
405 4         190 print STDERR "Made cipher\n";
406              
407 4 100       29 if( $file ) {
408 3         13 $self->{file} = $file;
409 3         14 $self->load;
410             }
411              
412 4         23 return $self;
413             }
414              
415             =item * load
416              
417             This overrides the C from L. There are a few
418             differences that matter.
419              
420             The Cookies database for Chrome tracks many more things than L
421             knows about, so this shoves everything into the "rest" hash. Notably:
422              
423             =over 4
424              
425             =item * Chrome sets the port to -1 if the cookie does not specify the port.
426              
427             =item * The value of the cookie is either the plaintext value or the decrypted value from C.
428              
429             =item * If C is set, this ignores the C<$maxage> part of L, but remembers the value in C.
430              
431             =back
432              
433             =cut
434              
435             sub load {
436 7     7 1 114 my( $self, $file ) = @_;
437              
438 7   100     76 $file ||= $self->{'file'} || return;
      66        
439              
440             # $cookie_jar->set_cookie( $version, $key, $val, $path,
441             # $domain, $port, $path_spec, $secure, $maxage, $discard, \%rest )
442              
443 3         17 my $rows = $self->_get_rows( $file );
444              
445 3         17 foreach my $row ( @$rows ) {
446 69 50       297 my $value = length $row->value ? $row->value : $row->decrypted_value;
447              
448             # if $max_page is not defined, HTTP::Cookies will not remove
449             # the cookies. We still track the actual value in the the
450             # hash and we can put the original back in place.
451 69         132 my $max_age = do {
452 69 50       133 if( $self->{ignore_discard} ) { undef }
  0         0  
453 69         228 else { ($row->expires_utc / 1_000_000) - time }
454             };
455              
456             # I've noticed that Chrome sets most ports to -1
457 69 50       236 my $port = $row->source_port > 0 ? $row->source_port : 80;
458              
459             my $rc = $self->set_cookie(
460             undef, # version
461             $row->name, # key
462             $value, # value
463             $row->path, # path
464             $row->host_key, # domain
465             $row->source_port, # port
466             undef, # path spec
467             $row->is_secure, # secure
468             $max_age, # max_age
469             0, # discard
470             {
471 69         274 map { $_ => $row->$_() } qw(
  897         2945  
472             value
473             creation_utc
474             is_httponly
475             last_access_utc
476             expires_utc
477             has_expires
478             is_persistent
479             priority
480             encrypted_value
481             samesite
482             source_scheme
483             is_same_party
484             source_port
485             )
486             }
487             );
488              
489             }
490              
491 3         37 1;
492             }
493              
494             =back
495              
496             =head2 Instance Methods
497              
498             =over 4
499              
500             =item * save( [ FILE ] )
501              
502             With no argument, save the cookies to the original filename. With
503             a file name argument, write the cookies to that filename. This will
504             be a SQLite database.
505              
506             =cut
507              
508             sub save {
509 1     1 1 960 my( $self, $new_file ) = @_;
510              
511 1   0     4 $new_file ||= $self->{'file'} || return;
      33        
512              
513 1         4 my $dbh = $self->_connect( $new_file );
514              
515 1         7 $self->_create_table;
516 1         38710 $self->_prepare_insert;
517 1         220 $self->_filter_cookies;
518 1         231 $dbh->disconnect;
519              
520 1         19 1;
521             }
522              
523             =item * set_cookie
524              
525             Overrides the C in L so it can ignore
526             the port check. Chrome uses C<-1> as the port if the cookie did not
527             specify a port. This version of C does no port check.
528              
529             =cut
530              
531             # We have to override this part because Chrome has -1 as a valid
532             # port value (for "unspecified port"). Otherwise this is lifted from
533             # HTTP::Cookies
534             sub set_cookie
535             {
536 69     69 1 132 my $self = shift;
537 69         190 my($version,
538             $key, $val, $path, $domain, $port,
539             $path_spec, $secure, $maxage, $discard, $rest) = @_;
540              
541             # path and key can not be empty (key can't start with '$')
542 69 50 33     552 return $self if !defined($path) || $path !~ m,^/, ||
      33        
543             !defined($key) || $key =~ m,^\$,;
544              
545             # ensure legal port
546 69         133 if (0 && defined $port) { # nerf this part
547             return $self unless $port =~ /^_?\d+(?:,\d+)*$/;
548             }
549              
550 69         102 my $expires;
551 69 50       135 if (defined $maxage) {
552 69 50       151 if ($maxage <= 0) {
553 0         0 delete $self->{COOKIES}{$domain}{$path}{$key};
554 0         0 return $self;
555             }
556 69         135 $expires = time() + $maxage;
557             }
558 69 50       141 $version = 0 unless defined $version;
559              
560 69         548 my @array = ($version, $val,$port,
561             $path_spec,
562             $secure, $expires, $discard);
563 69 50 33     627 push(@array, {%$rest}) if defined($rest) && %$rest;
564             # trim off undefined values at end
565 69         235 pop(@array) while !defined $array[-1];
566              
567 69         237 $self->{COOKIES}{$domain}{$path}{$key} = \@array;
568 69         374 $self;
569             }
570              
571 0         0 BEGIN {
572             package HTTP::Cookies::Chrome::Record;
573 4     4   334580 use vars qw($AUTOLOAD);
  4         11  
  4         1252  
574              
575 4     4   21 my %columns = map { state $n = 0; $_, $n++ } qw(
  72         96  
  72         414  
576             creation_utc
577             host_key
578             name
579             value
580             path
581             expires_utc
582             is_secure
583             is_httponly
584             last_access_utc
585             has_expires
586             is_persistent
587             priority
588             encrypted_value
589             samesite
590             source_scheme
591             source_port
592             is_same_party
593             decrypted_value
594             );
595              
596             sub new {
597 92     92   173 my( $class, $array ) = @_;
598 92         207 bless $array, $class;
599             }
600              
601             sub decrypted_value {
602 161     161   330 my( $self, $value ) = @_;
603              
604 161 100       382 return $self->[ $columns{decrypted_value} ] unless defined $value;
605 92         341 $self->[ $columns{decrypted_value} ] = $value;
606             }
607              
608             sub AUTOLOAD {
609 1610     1610   2979 my( $self ) = @_;
610 1610         2529 my $method = $AUTOLOAD;
611 1610         5079 $method =~ s/.*:://;
612              
613 1610 50       3996 die "No method <$method>" unless exists $columns{$method};
614              
615 1610         5181 $self->[ $columns{$method} ];
616             }
617              
618 92     92   9066 sub DESTROY { 1 }
619             }
620              
621             =back
622              
623             =head2 Getting the Chrome Safe Storage password
624              
625             You can get the Chrome Safe Storage password, although you may have to
626             respond to other dialogs and features of its storage mechanism:
627              
628             On macOS:
629              
630             % security find-generic-password -a "Chrome" -w
631             % security find-generic-password -a "Brave" -w
632              
633             On Ubuntu using libsecret:
634              
635             % secret-tool lookup xdg:schema chrome_libsecret_os_crypt_password application chrome
636             % secret-tool lookup xdg:schema chrome_libsecret_os_crypt_password application brave
637              
638             If you know of other methods, let me know.
639              
640             Some useful information:
641              
642             =over 4
643              
644             =item * On Linux systems not using a keychain, the password might be C
645             or C. Maybe I should use L
646              
647             =item * L
648              
649             =item * L
650              
651             =item * L
652              
653             =back
654              
655             =head2 The Chrome cookies table
656              
657             creation_utc INTEGER NOT NULL UNIQUE PRIMARY KEY
658             host_key TEXT NOT NULL
659             name TEXT NOT NULL
660             value TEXT NOT NULL
661             path TEXT NOT NULL
662             expires_utc INTEGER NOT NULL
663             is_secure INTEGER NOT NULL
664             is_httponly INTEGER NOT NULL
665             last_access_utc INTEGER NOT NULL
666             has_expires INTEGER NOT NULL
667             is_persistent INTEGER NOT NULL
668             priority INTEGER NOT NULL
669             encrypted_value BLOB
670             samesite INTEGER NOT NULL
671             source_scheme INTEGER NOT NULL
672             source_port INTEGER NOT NULL
673             is_same_party INTEGER NOT NULL
674              
675             =head1 TO DO
676              
677             There are many ways that this module can approve.
678              
679             1. The L module was written a long time ago. We still
680             inherit from it, but it might be time to completely dump it even if
681             we keep the interface.
682              
683             2. Some Windows people can fill in the Windows details for C
684             and C.
685              
686             3. As in (2), systems that aren't Linux or macOS can fill in their details.
687              
688             4. We need a way to specify a new password to output the cookies to a
689             different Chrome-like SQLite database. The easiest thing right now might be
690             to make a completely new object with the new password and load cookies
691             into it.
692              
693             =head1 SOURCE AVAILABILITY
694              
695             This module is in Github:
696              
697             https://github.com/briandfoy/http-cookies-chrome
698              
699             =head1 AUTHOR
700              
701             brian d foy, C<< >>
702              
703             =head1 CREDITS
704              
705             Jon Orwant pointed out the problem with dates too far in the future
706              
707             =head1 COPYRIGHT AND LICENSE
708              
709             Copyright © 2009-2021, brian d foy . All rights reserved.
710              
711             This program is free software; you can redistribute it and/or modify
712             it under the terms of the Artistic License 2.0.
713              
714             =cut
715              
716             1;