File Coverage

blib/lib/HTTP/Cookies/Chrome.pm
Criterion Covered Total %
statement 235 265 88.6
branch 41 86 47.6
condition 26 59 44.0
subroutine 44 47 93.6
pod 6 6 100.0
total 352 463 76.0


line stmt bran cond sub pod time code
1 5     5   1935601 use 5.010;
  5         21  
2 5     5   3107 use utf8;
  5         1770  
  5         50  
3              
4             package HTTP::Cookies::Chrome;
5 5     5   224 use strict;
  5         14  
  5         108  
6              
7 5     5   19 use warnings;
  5         12  
  5         257  
8 5     5   36 use warnings::register;
  5         9  
  5         820  
9              
10 5     5   2947 use POSIX;
  5         42679  
  5         34  
11              
12             BEGIN {
13 5     5   47 my @names = qw( _VERSION KEY VALUE PATH DOMAIN PORT PATH_SPEC
14             SECURE EXPIRES DISCARD REST );
15 5         15 my $n = 0;
16 5         17 foreach my $name ( @names ) {
17 5     5   17498 no strict 'refs';
  5         17  
  5         589  
18 55         80 my $m = $n++;
19 55         480 *{$name} = sub () { $m }
  0         0  
20 55         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             =head2 Notes on modernization
73              
74             HTTP::Cookies::Chrome v3 works with version 24 of the Chrome cookies
75             database. If you need something earlier, use an older version of this
76             module.
77              
78             The Chrome cookie database version 24 changed how it stored encrypted
79             cookies. It takes the SHA256 of the domain and prepends the binary value
80             to the plaintext of the cookie value. It then encrypts the entire thing,
81             and puts C in front of it as before.
82              
83             So far, this module has been updated to read that format and discard the
84             SHA256 rather than verifying its value if correct, or at least passing
85             that on so the user can verify it themselves. This module should do
86             either or both of those.
87              
88             Also, this module has not yet been updated to write it out in the same
89             way.
90              
91             =cut
92              
93 5     5   37 use base qw( HTTP::Cookies );
  5         8  
  5         7238  
94 5     5   100136 use vars qw( $VERSION );
  5         14  
  5         359  
95              
96 5     5   37 use constant TRUE => 1;
  5         12  
  5         426  
97 5     5   30 use constant FALSE => 0;
  5         10  
  5         314  
98              
99             $VERSION = '3.002';
100              
101 5     5   17694 use DBI;
  5         142060  
  5         10267  
102              
103             sub _add_value {
104 8     8   31 my( $self, $key, $value ) = @_;
105 8         45 $self->_stash->{$key} = $value;
106             }
107              
108 253     253   779 sub _cipher { $_[0]->_get_value( 'cipher' ) }
109              
110             sub _connect {
111 5     5   32 my( $self, $file ) = @_;
112 5         99 my $dbh = DBI->connect( "dbi:SQLite:dbname=$file", '', '',
113             {
114             sqlite_see_if_its_a_number => 1,
115             } );
116 5         90787 $_[0]->{dbh} = $dbh;
117             }
118              
119             sub _create_table {
120 1     1   4 my( $self ) = @_;
121              
122 1         6 $self->_dbh->do( 'DROP TABLE IF EXISTS cookies' );
123 1         114 $self->_dbh->do( 'DROP TABLE IF EXISTS meta' );
124              
125 1         42 $self->_dbh->do( q(CREATE TABLE meta(key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY, value LONGVARCHAR)) );
126 1         26307 $self->_dbh->do( q(INSERT INTO meta VALUES('mmap_status','-1')) );
127 1         33029 $self->_dbh->do( q(INSERT INTO meta VALUES('version','24')) );
128 1         26014 $self->_dbh->do( q(INSERT INTO meta VALUES('last_compatible_version','24q')) );
129              
130 1         40985 $self->_dbh->do( <<'SQL' );
131             CREATE TABLE cookies (
132             creation_utc INTEGER NOT NULL,
133             host_key TEXT NOT NULL,
134             top_frame_site_key TEXT NOT NULL,
135             name TEXT NOT NULL,
136             value TEXT NOT NULL,
137             encrypted_value BLOB NOT NULL,
138             path TEXT NOT NULL,
139             expires_utc INTEGER NOT NULL,
140             is_secure INTEGER NOT NULL,
141             is_httponly INTEGER NOT NULL,
142             last_access_utc INTEGER NOT NULL,
143             has_expires INTEGER NOT NULL,
144             is_persistent INTEGER NOT NULL,
145             priority INTEGER NOT NULL,
146             samesite INTEGER NOT NULL,
147             source_scheme INTEGER NOT NULL,
148             source_port INTEGER NOT NULL,
149             last_update_utc INTEGER NOT NULL,
150             source_type INTEGER NOT NULL,
151             has_cross_site_ancestor INTEGER NOT NULL
152             );
153             SQL
154              
155 1         29251 $self->_dbh->do( <<'SQL' );
156             CREATE UNIQUE INDEX cookies_unique_index ON cookies(
157             host_key,
158             top_frame_site_key,
159             has_cross_site_ancestor,
160             name,
161             path,
162             source_scheme,
163             source_port
164             );
165             SQL
166             }
167              
168 15     15   228 sub _dbh { $_[0]->{dbh} }
169              
170             sub _db_24_or_later {
171 92     92   179 my( $self ) = shift;
172              
173 92         153 state $yes;
174 92 100       351 return $yes if defined $yes;
175              
176 3         69 my $v = $self->_db_version;
177 3 50       31 $yes = defined $v and $v >= 24;
178             }
179              
180             sub _db_version {
181 3     3   45 my( $self ) = @_;
182              
183 3         11 state $has_meta = eval {
184 3         8 my $sql = q(SELECT 1 FROM sqlite_master WHERE type='table' AND name='meta');
185 3         15 my $array = $self->_dbh->selectall_arrayref( $sql );
186 3         630 @$array > 0;
187             };
188 3 50       19 if( $@ ) { warn $@ }
  0         0  
189              
190 3         7 state $version;
191 3 50       12 return $version if defined $version;
192 3 50       27 return unless $has_meta;
193              
194 3   50     6 $version = eval {
195 3         8 my $key = 'version';
196 3         8 my $sql = qq(SELECT * FROM meta WHERE key = '$key');
197 3         14 my $rv = $self->_dbh->selectall_arrayref( $sql );
198 3 50       558 @$rv ? $rv->[0][1] : 0;
199             } // 0;
200 3 50       37 if( $@ ) { warn $@ }
  0         0  
201              
202 3         11 return $version;
203             };
204              
205             sub _decrypt {
206 92     92   180 my( $self, $blob ) = @_;
207              
208 92 50       574 unless( $self->_cipher ) {
209 0 0       0 warnings::warn("Decrypted cookies is not set up") if warnings::enabled();
210 0         0 return;
211             }
212              
213 92         239 my $type = substr $blob, 0, 3, '';
214              
215 92 50       214 unless( $type eq 'v10' ) { # v11 is a thing, too
216 0 0       0 warnings::warn("Encrypted value is unexpected type <$type>") if warnings::enabled();
217 0         0 return;
218             }
219              
220 92         174 my $plaintext = $self->_cipher->decrypt( $blob );
221 92         194 my $sha256;
222              
223 92 50       307 if( $self->_db_24_or_later ) {
224 92         188 $sha256 = substr $plaintext, 0, 32, '';
225             }
226              
227             # padding is always added to get to a multiple of 16. If the value is already
228             # a multiple of 16, it's padded by an additional 16 octets.
229 92         222 my $padding_count = ord( substr $plaintext, -1 );
230              
231 92         190 my $padding = substr( $plaintext, -$padding_count );
232              
233 92 100 100     1834 unless( $padding =~ /\A(.){$padding_count}\z/ and ord(substr $padding, 0, 1) == $padding_count ) {
234 69 50       5474 warnings::warn("Unexpected padding in encrypted cookie") if warnings::enabled();
235             }
236              
237 92 100       323 substr( $plaintext, -$padding_count ) = '' if $padding_count <= 16;
238              
239 92         359 ( $plaintext, $sha256 );
240             }
241              
242             sub _encrypt {
243 23     23   67 my( $self, $value ) = @_;
244              
245 23 50       86 unless( defined $value ) {
246 0 0       0 warnings::warn("Value is not defined! Nothing to encrypt!") if warnings::enabled();
247 0         0 return;
248             }
249              
250 23 50       67 unless( $self->_cipher ) {
251 0 0       0 warnings::warn("Encrypted cookies is not set up") if warnings::enabled();
252 0         0 return;
253             }
254              
255 23         48 my $blocksize = 16;
256              
257             # padding is always added. If the length is already a multiple of 16, the
258             # padding is the same as the blocksize.
259 23         77 my $padding_length = ($blocksize - length($value) % $blocksize);
260 23 50       68 $padding_length = $blocksize if $padding_length == 0;
261 23         93 my $padding = chr($padding_length) x $padding_length;
262 23         63 my $encrypted = 'v10' . $self->_cipher->encrypt( $value . $padding );
263              
264 23         87 $encrypted;
265             }
266              
267             sub _filter_cookies {
268 1     1   3 my( $self ) = @_;
269              
270             $self->scan(
271             sub {
272 23     23   883 my( $version, $key, $val, $path, $domain, $port,
273             $path_spec, $secure, $expires, $discard, $rest ) = @_;
274              
275 23         123 my @parts = @_;
276              
277 23 50 33     110 return if $parts[DISCARD] && not $self->{ignore_discard};
278 23 50 33     201 return if defined $parts[EXPIRES] && time > $parts[EXPIRES];
279              
280 23         90 $parts[EXPIRES] = $rest->{expires_utc};
281 23 100       92 $parts[SECURE] = $parts[SECURE] ? TRUE : FALSE;
282              
283 23 100       146 my $bool = $domain =~ /^\./ ? TRUE : FALSE;
284              
285 23         130 $self->_insert( @parts );
286             }
287 1         24 );
288             }
289              
290             sub _get_rows {
291 4     4   1276 my( $self, $file ) = @_;
292              
293 4         21 my $dbh = $self->_connect( $file );
294              
295 4         40 my $sth = $dbh->prepare( <<'SQL' );
296             SELECT
297             creation_utc,
298             host_key,
299             name,
300             value,
301             path,
302             expires_utc,
303             is_secure,
304             is_httponly,
305             last_access_utc,
306             has_expires,
307             is_persistent,
308             priority,
309             encrypted_value,
310             samesite,
311             source_scheme,
312             source_port
313             FROM
314             cookies
315             SQL
316              
317 4         5534 $sth->execute;
318              
319             my @rows =
320             map {
321 92 50       508 if( my $e = $_->encrypted_value ) {
322 92         253 my( $p, $sha256 ) = $self->_decrypt( $e );
323 92         467 $_->decrypted_value( $p );
324             }
325 92         277 $_;
326             }
327 92         215 map { HTTP::Cookies::Chrome::Record->new( $_ ) }
328 4         16 @{ $sth->fetchall_arrayref };
  4         845  
329              
330 4         597 $dbh->disconnect;
331              
332 4         97 \@rows;
333             }
334              
335             sub _get_value {
336 253     253   530 my( $self, $key ) = @_;
337 253         684 $self->_stash->{$key}
338             }
339              
340             {
341             my $creation_offset = 0;
342              
343             sub _insert {
344 23     23   112 my( $self, @parts ) = @_;
345              
346 23         54 my $rest = $parts[REST];
347              
348 23   50     148 $rest->{httponly} //= 0;
349 23   33     176 $rest->{last_update_utc} //= time() * 1000;
350 23   50     89 $rest->{samesite} //= -1; # magic value for unspecified
351 23   50     78 $rest->{source_scheme} //= '';
352 23   50     145 $rest->{source_type} //= '';
353 23   50     129 $rest->{top_frame_site_key} //= '';
354 23   50     164 $rest->{has_cross_site_ancestor} //= 0;
355              
356             # possibly thinking about a feature to remove the encryption,
357             # so we'd need to re-encrypt things. Here we assume that already
358             # exists so we always re-encrypt.
359 23         55 my $encrypted_value = '';
360              
361             # If we have a value and there was a previous encrypted value,
362             # encrypted the current value and blank out the value. Other
363 23 50 33     262 if( $parts[VALUE] and $rest->{encrypted_value} and $self->_cipher ) {
      33        
364 23         105 $encrypted_value = $self->_encrypt( $parts[VALUE] );
365 23         64 $parts[VALUE] = '';
366             }
367              
368             # Some cookies don't have values. WTF?
369 23   50     90 $parts[VALUE] //= '';
370              
371             my @values = (
372             $rest->{creation_utc},
373             $parts[DOMAIN],
374             $rest->{top_frame_site_key},
375             @parts[KEY, VALUE],
376             $encrypted_value,
377             $parts[PATH],
378             $rest->{expires_utc},
379             $parts[SECURE],
380 23         133 @{ $rest }{ qw(is_httponly last_access_utc has_expires is_persistent priority samesite source_scheme) },
381             $parts[PORT],
382 23         140 @{$rest}{qw(last_update_utc source_type has_cross_site_ancestor)},
  23         169  
383             );
384              
385 23         583127 $self->{insert_sth}->execute( @values );
386             }
387             }
388              
389             sub _get_utc_microseconds {
390 5     5   90 no warnings 'uninitialized';
  5         51  
  5         469  
391 5     5   4412 use bignum;
  5         20566  
  5         82  
392 0   0 0   0 POSIX::strftime( '%s', gmtime() ) * 1_000_000 + ($_[1]//0);
393             }
394              
395             sub _make_cipher {
396 4     4   59 my( $self, $password ) = @_;
397              
398 4         10 my $key = do {
399 4         7505 state $rc2 = require PBKDF2::Tiny;
400 4         20982 my $s = $self->_platform_settings;
401 4         12 my $salt = 'saltysalt';
402 4         12 my $length = 16;
403 4         27 PBKDF2::Tiny::derive( 'SHA-1', $password, $salt, $s->{iterations}, $length );
404             };
405              
406 4         4041 state $rc1 = require Crypt::Rijndael;
407 4         6073 my $cipher = Crypt::Rijndael->new( $key, Crypt::Rijndael::MODE_CBC() );
408 4         38 $cipher->set_iv( ' ' x 16 );
409              
410 4         35 $self->_add_value( chrome_safe_storage_password => $password );
411 4         29 $self->_add_value( cipher => $cipher );
412             }
413              
414             sub _platform_settings {
415             # https://n8henrie.com/2014/05/decrypt-chrome-cookies-with-python/
416             # https://github.com/n8henrie/pycookiecheat/issues/12
417 4     4   38 state $settings = {
418             darwin => {
419             iterations => 1003,
420             },
421             linux => {
422             iterations => 1,
423             },
424             MSWin32 => {
425             },
426             };
427              
428 4         25 $settings->{$^O};
429             }
430              
431             sub _prepare_insert {
432 1     1   4 my( $self ) = @_;
433 1         14 state $columns = [qw(
434             creation_utc
435             host_key
436             top_frame_site_key
437             name
438             value
439             encrypted_value
440             path
441             expires_utc
442             is_secure
443             is_httponly
444             last_access_utc
445             has_expires
446             is_persistent
447             priority
448             samesite
449             source_scheme
450             source_port
451             last_update_utc
452             source_type
453             has_cross_site_ancestor
454             )];
455 1         9 state $columns_str = join ', ', @$columns;
456 1         6 state $placeholders = join ', ', ('?') x @$columns;
457 1         7 my $sth = $self->{insert_sth} = $self->_dbh->prepare_cached( <<"SQL" );
458             INSERT INTO cookies ($columns_str) VALUES ( $placeholders )
459             SQL
460              
461             }
462              
463             sub _stash {
464 261     261   506 state $mod_key = 'X-CHROME';
465 261   100     2258 $_[0]->{$mod_key} //= {};
466             }
467              
468             =head2 Class methods
469              
470             =over 4
471              
472             =item * guess_password
473              
474             Try to retrieve the Chrome Safe Storage password by accessing the
475             system secrets for the logged-in user. This returns nothing if it
476             can't find it.
477              
478             You don't need to use this to get the password.
479              
480             On macOS, this looks in the Keyring using C.
481              
482             On Linux, this uses C, which you might have to install
483             separately. Also, some early versions used the hard-coded password
484             C, and some others may have used C.
485              
486             I don't know how to do this on Windows. If you know, send a pull request.
487             That goes for other systems too.
488              
489             =cut
490              
491             sub guess_password {
492 0     0 1 0 my $p = do {
493 0 0       0 if( $^O eq 'darwin' ) { `security find-generic-password -a "Chrome" -w` }
  0 0       0  
494 0         0 elsif( $^O eq 'linux' ) { `secret-tool lookup xdg:schema chrome_libsecret_os_crypt_password application chrome` }
495             };
496 0         0 chomp $p;
497 0         0 $p
498             }
499              
500             =item * guess_path( PROFILE )
501              
502             Try to retrieve the directory that contains the Cookies file. If you
503             don't specify C, it uses C.
504              
505             macOS: F<~/Library/Application Support/Google/Chrome/PROFILE/Cookies>
506              
507             Linux: F<~/.config/google-chrome/PROFILE/Cookies>
508              
509             =cut
510              
511             sub guess_path {
512 0     0 1 0 my( $self, $profile ) = @_;
513 0   0     0 $profile //= 'Default';
514              
515 0         0 my $path_to_cookies = do {
516 0 0       0 if( $^O eq 'darwin' ) { "$ENV{HOME}/Library/Application Support/Google/Chrome/$profile/Cookies" }
  0 0       0  
517 0         0 elsif( $^O eq 'linux' ) { "$ENV{HOME}/.config/google-chrome/$profile/Cookies" }
518             };
519              
520 0 0       0 return unless -e $path_to_cookies;
521 0         0 $path_to_cookies
522             }
523              
524             =item * new
525              
526             The extends the C in L, with the additional parameter
527             for the decryption password.
528              
529             chrome_safe_storage_password - the password
530              
531             =cut
532              
533             sub new {
534 4     4 1 28085 my( $class, %args ) = @_;
535              
536 4         19 my $pass = delete $args{chrome_safe_storage_password};
537 4         36 my $file = delete $args{file};
538              
539 4         75 my $self = $class->SUPER::new( %args );
540              
541 4 50       30 return $self unless defined $pass;
542              
543 4         33 $self->_make_cipher( $pass );
544              
545 4 100       15 if( $file ) {
546 3         27 $self->{file} = $file;
547 3         19 $self->load;
548             }
549              
550 4         45 return $self;
551             }
552              
553             =item * load
554              
555             This overrides the C from L. There are a few
556             differences that matter.
557              
558             The Cookies database for Chrome tracks many more things than L
559             knows about, so this shoves everything into the "rest" hash. Notably:
560              
561             =over 4
562              
563             =item * Chrome sets the port to -1 if the cookie does not specify the port.
564              
565             =item * The value of the cookie is either the plaintext value or the decrypted value from C.
566              
567             =item * If C is set, this ignores the C<$maxage> part of L, but remembers the value in C.
568              
569             =back
570              
571             =cut
572              
573             sub load {
574 7     7 1 112 my( $self, $file ) = @_;
575              
576 7   100     86 $file ||= $self->{'file'} || return;
      66        
577              
578             # $cookie_jar->set_cookie( $version, $key, $val, $path,
579             # $domain, $port, $path_spec, $secure, $maxage, $discard, \%rest )
580              
581 3         16 my $rows = $self->_get_rows( $file );
582              
583 3         30 foreach my $row ( @$rows ) {
584 69 50       327 my $value = length $row->value ? $row->value : $row->decrypted_value;
585              
586             # if $max_page is not defined, HTTP::Cookies will not remove
587             # the cookies. We still track the actual value in the the
588             # hash and we can put the original back in place.
589 69         135 my $max_age = do {
590 69 50       183 if( $self->{ignore_discard} ) { undef }
  0         0  
591 69         280 else { ($row->expires_utc / 1_000_000) - time }
592             };
593              
594             # I've noticed that Chrome sets most ports to -1
595 69 50       225 my $port = $row->source_port > 0 ? $row->source_port : 80;
596              
597             my $rc = $self->set_cookie(
598             undef, # version
599             $row->name, # key
600             $value, # value
601             $row->path, # path
602             $row->host_key, # domain
603             $row->source_port, # port
604             undef, # path spec
605             $row->is_secure, # secure
606             $max_age, # max_age
607             0, # discard
608             {
609 69         246 map { $_ => $row->$_() } qw(
  897         7734  
610             value
611             creation_utc
612             is_httponly
613             last_access_utc
614             expires_utc
615             has_expires
616             is_persistent
617             priority
618             encrypted_value
619             samesite
620             source_scheme
621             is_same_party
622             source_port
623             )
624             }
625             );
626              
627             }
628              
629 3         21 1;
630             }
631              
632             =back
633              
634             =head2 Instance Methods
635              
636             =over 4
637              
638             =item * save( [ FILE ] )
639              
640             With no argument, save the cookies to the original filename. With
641             a file name argument, write the cookies to that filename. This will
642             be a SQLite database.
643              
644             =cut
645              
646             sub save {
647 1     1 1 2651 my( $self, $new_file ) = @_;
648              
649 1   0     4 $new_file ||= $self->{'file'} || return;
      33        
650              
651 1         6 my $dbh = $self->_connect( $new_file );
652              
653 1         9 $self->_create_table;
654 1         35141 $self->_prepare_insert;
655 1         259 $self->_filter_cookies;
656 1         230 $dbh->disconnect;
657              
658 1         12 1;
659             }
660              
661             =item * set_cookie
662              
663             Overrides the C in L so it can ignore
664             the port check. Chrome uses C<-1> as the port if the cookie did not
665             specify a port. This version of C does no port check.
666              
667             =cut
668              
669             # We have to override this part because Chrome has -1 as a valid
670             # port value (for "unspecified port"). Otherwise this is lifted from
671             # HTTP::Cookies
672             sub set_cookie {
673 69     69 1 137 my $self = shift;
674 69         226 my($version,
675             $key, $val, $path, $domain, $port,
676             $path_spec, $secure, $maxage, $discard, $rest) = @_;
677              
678             # path and key can not be empty (key can't start with '$')
679 69 50 33     580 return $self if !defined($path) || $path !~ m,^/, ||
      33        
680             !defined($key) || $key =~ m,^\$,;
681              
682             # ensure legal port
683 69         107 if (0 && defined $port) { # nerf this part
684             return $self unless $port =~ /^_?\d+(?:,\d+)*$/;
685             }
686              
687 69         135 my $expires;
688 69 50       141 if (defined $maxage) {
689 69 50       212 if ($maxage <= 0) {
690 0         0 delete $self->{COOKIES}{$domain}{$path}{$key};
691 0         0 return $self;
692             }
693 69         144 $expires = time() + $maxage;
694             }
695 69 50       151 $version = 0 unless defined $version;
696              
697 69         187 my @array = ($version, $val,$port,
698             $path_spec,
699             $secure, $expires, $discard);
700 69 50 33     794 push(@array, {%$rest}) if defined($rest) && %$rest;
701             # trim off undefined values at end
702 69         231 pop(@array) while !defined $array[-1];
703              
704 69         271 $self->{COOKIES}{$domain}{$path}{$key} = \@array;
705 69         576 $self;
706             }
707              
708 0         0 BEGIN {
709             package HTTP::Cookies::Chrome::Record;
710 5     5   801218 use vars qw($AUTOLOAD);
  5         16  
  5         870  
711              
712 5     5   41 my %columns = map { state $n = 0; $_, $n++ } qw(
  90         122  
  90         663  
713             creation_utc
714             host_key
715             name
716             value
717             path
718             expires_utc
719             is_secure
720             is_httponly
721             last_access_utc
722             has_expires
723             is_persistent
724             priority
725             encrypted_value
726             samesite
727             source_scheme
728             source_port
729             is_same_party
730             decrypted_value
731             );
732              
733 5     5   4533 use Data::Dumper;
  5         52542  
  5         1930  
734             sub new {
735 92     92   147 my( $class, $array ) = @_;
736 92         191 bless $array, $class;
737             }
738              
739             sub decrypted_value {
740 161     161   312 my( $self, $value ) = @_;
741              
742 161 100       515 return $self->[ $columns{decrypted_value} ] unless defined $value;
743 92         386 $self->[ $columns{decrypted_value} ] = $value;
744             }
745              
746             sub AUTOLOAD {
747 1610     1610   3179 my( $self ) = @_;
748 1610         4515 my $method = $AUTOLOAD;
749 1610         4631 $method =~ s/.*:://;
750              
751 1610 50       3415 die "No method <$method>" unless exists $columns{$method};
752              
753 1610         5413 $self->[ $columns{$method} ];
754             }
755              
756 92     92   40262 sub DESTROY { 1 }
757             }
758              
759             =back
760              
761             =head2 Getting the Chrome Safe Storage password
762              
763             You can get the Chrome Safe Storage password, although you may have to
764             respond to other dialogs and features of its storage mechanism:
765              
766             On macOS:
767              
768             % security find-generic-password -a "Chrome" -w
769             % security find-generic-password -a "Brave" -w
770              
771             On Ubuntu using libsecret:
772              
773             % secret-tool lookup xdg:schema chrome_libsecret_os_crypt_password application chrome
774             % secret-tool lookup xdg:schema chrome_libsecret_os_crypt_password application brave
775              
776             If you know of other methods, let me know.
777              
778             Some useful information:
779              
780             =over 4
781              
782             =item * On Linux systems not using a keychain, the password might be C
783             or C. Maybe I should use L
784              
785             =item * L
786              
787             =item * L
788              
789             =item * L
790              
791             =back
792              
793             =head2 The Chrome cookies table
794              
795             creation_utc INTEGER NOT NULL UNIQUE PRIMARY KEY
796             host_key TEXT NOT NULL
797             name TEXT NOT NULL
798             value TEXT NOT NULL
799             path TEXT NOT NULL
800             expires_utc INTEGER NOT NULL
801             is_secure INTEGER NOT NULL
802             is_httponly INTEGER NOT NULL
803             last_access_utc INTEGER NOT NULL
804             has_expires INTEGER NOT NULL
805             is_persistent INTEGER NOT NULL
806             priority INTEGER NOT NULL
807             encrypted_value BLOB
808             samesite INTEGER NOT NULL
809             source_scheme INTEGER NOT NULL
810             source_port INTEGER NOT NULL
811             is_same_party INTEGER NOT NULL
812              
813             =head1 TO DO
814              
815             There are many ways that this module can approve.
816              
817             1. The L module was written a long time ago. We still
818             inherit from it, but it might be time to completely dump it even if
819             we keep the interface.
820              
821             2. Some Windows people can fill in the Windows details for C
822             and C.
823              
824             3. As in (2), systems that aren't Linux or macOS can fill in their details.
825              
826             4. We need a way to specify a new password to output the cookies to a
827             different Chrome-like SQLite database. The easiest thing right now might be
828             to make a completely new object with the new password and load cookies
829             into it.
830              
831             =head1 SOURCE AVAILABILITY
832              
833             This module is in Github:
834              
835             https://github.com/briandfoy/http-cookies-chrome
836              
837             =head1 AUTHOR
838              
839             brian d foy, C<< >>
840              
841             =head1 CREDITS
842              
843             Jon Orwant pointed out the problem with dates too far in the future
844              
845             =head1 COPYRIGHT AND LICENSE
846              
847             Copyright © 2009-2025, brian d foy . All rights reserved.
848              
849             This program is free software; you can redistribute it and/or modify
850             it under the terms of the Artistic License 2.0.
851              
852             =cut
853              
854             1;