File::KeePass(3pm)

πŸ“¦
version 2.03 Β· 6 versions
, in debian bookworm, debian trixie, ubuntu jammy, ubuntu noble

πŸ’– File::KeePass(3pm) πŸ’–

okay bestie, gather round, because this is the manual page for File::KeePass β€” like, the actual File::KeePass(3pm) page from your friendly neighbourhood perl docs (User Contributed Perl Documentation section, if you're keeping track πŸ“š). She's a big one so buckle up, this is part one of three 🫢


✨ NAME ✨

File::KeePass β€” the interface to KeePass V1 and V2 database files, no cap πŸ’…


🌸 SYNOPSIS 🌸

use File::KeePass;
use Data::Dumper qw(Dumper);

my $k = File::KeePass->new;

# read a version 1 or version 2 database
$k->load_db($file, $master_pass); # errors die

print Dumper $k->header;
print Dumper $k->groups; # passwords are locked

$k->unlock;
print Dumper $k->groups; # passwords are now visible

$k->clear; # delete current db from memory

my $group = $k->add_group({
    title => 'Foo',
}); # root level group
my $gid = $group->{'id'};

my $group = $k->find_group({id => $gid});
# OR
my $group = $k->find_group({title => 'Foo'});

my $group2 = $k->add_group({
    title => 'Bar',
    group => $gid,
    # OR group => $group,
}); # nested group

my $e = $k->add_entry({
    title    => 'Something',
    username => 'someuser',
    password => 'somepass',
    group    => $gid,
    # OR group => $group,
});
my $eid = $e->{'id'};

my $e = $k->find_entry({id => $eid});
# OR
my $e = $k->find_entry({title => 'Something'});

$k->lock;
print $e->{'password'}; # eq undef
print $k->locked_entry_password($e); # eq 'somepass'

$k->unlock;
print $e->{'password'}; # eq 'somepass'

# save out a version 1 database
$k->save_db("/some/file/location.kdb", $master_pass);

# save out a version 2 database
$k->save_db("/some/file/location.kdbx", $master_pass);

# save out a version 1 database using a password and key file
$k->save_db("/some/file/location.kdb", [$master_pass, $key_filename]);

# read database from a file
$k->parse_db($pass_db_string, $pass);

# generate a keepass version 1 database string
my $pass_db_string = $k->gen_db($pass);

# generate a keepass version 2 database string
my $pass_db_string = $k->gen_db($pass);

πŸ¦‹ DESCRIPTION πŸ¦‹

File::KeePass gives you access to KeePass version 1 (kdb) and version 2 (kdbx) databases. Both of them, yeah. We contain multitudes πŸ’œ

So here's the tea: version 1 and version 2 databases are wildly different in how they're built, right? But like… most of the actual information overlaps, and a whole bunch of the algorithms are basically the same vibe. So File::KeePass does the work of ironing out as many of those differences as it possibly can for you. Iconic behaviour, honestly ✨

File::KeePass gives you nearly raw data access. There are a few little utility methods for messing with groups and entries, but the advanced manipulation stuff? That can easily be layered on top by other modules. We're not gonna gatekeep, we're just not gonna do it ourselves πŸŽ€

And important: File::KeePass is only for reading and writing databases and for keeping passwords scrambled while they're in memory. Programs dealing with UI, or with auto-type features? That's the domain of other modules on CPAN. File::KeePass::Agent is one example πŸ‘‘


πŸ’… METHODS πŸ’…

  • new β€” takes a hashref or hash of arguments. Returns a brand new File::KeePass object. Any named arguments you pass get added to self. Cute, simple, done πŸ’–
  • load_db β€” takes a kdb filename, a master password, and an optional argument hashref. Returns the File::KeePass object on success (and yes, you can call it as a class method 🫢). Errors die. The resulting database can be accessed via various methods, including $k->groups.
my $k = File::KeePass->new;
$k->load_db($file, $pwd);

my $k = File::KeePass->load_db($file, $pwd);

my $k = File::KeePass->load_db($file, $pwd, {auto_lock => 0});

The contents get read from the file and then passed along to parse_db. Chain of command, we love to see it 🌈

The password you pass to load_db can be a composite key in any of these forms, bestie:

"password"                   # password only
["password"]                 # same
["password", "keyfilename"]  # password and key file
[undef, "keyfilename"]       # key file only
["password", \"keycontent"]  # password and reference to key file content
[undef, \"keycontent"]       # reference to key file content only

The key file is optional. You can pass it as a filename, or as a scalar reference to the contents of the key file. If you pass a filename, it will be read in. And the key file can contain any of these three types:

length 32         # treated as raw key
length 64         # must be 64 hexidecimal characters
any-other-length  # a SHA256 sum will be taken of the data

(yes, it really says hexidecimal in the original, we're not touching that spelling, periodt πŸ™ˆ)

  • save_db β€” takes a kdb filename and a master password. Stores out the current groups that are chilling in the object. The writes attempt to write first to $file.new.$epoch and are then renamed into the correct location. Sneaky little atomic-ish move, we love her πŸ’…
⚠️ You will need to unlock the db via $k->unlock before calling this method if the database is currently locked. Don't skip this step, bestie, it will not go well 😭

The same master password types passed to load_db can be used here too πŸ’œ

  • parse_db β€” takes a string or a reference to a string containing an encrypted kdb database, a master password, and an optional argument hashref. Returns the File::KeePass object on success (can be called as a class method 🫢). Errors die. The resulting database can be accessed via various methods, including $k->groups.
my $k = File::KeePass->new;
$k->parse_db($loaded_kdb, $pwd);

my $k = File::KeePass->parse_db($kdb_buffer, $pwd);

my $k = File::KeePass->parse_db($kdb_buffer, $pwd, {auto_lock => 0});

And yep, the same master password types passed to load_db can be used here as well ✨

  • parse_header β€” used by parse_db. Reads just the header information. This one's handy as a basic KeePass file check βœ… The returned hash will contain version => 1 or version => 2, depending on which type of header it finds. Can be called as a class method.
my $head = File::KeePass->parse_header($kdb_buffer); # errors die
printf "This is a version %d database\n", $head->{'version'};
  • gen_db β€” takes a master password. Optionally takes a "groups" arrayref and a "headers" hashref. If groups are not passed, it defaults to using the currently loaded groups. If headers are not passed, a fresh set of headers gets generated based on the groups and the master password. The headers can be passed in to test round trip portability β€” cute for the paranoid girlies 🧚
⚠️ You will need to unlock the db via $k->unlock before calling this method if the database is currently locked.

The same master password types passed to load_db can be used here πŸ’–

  • header β€” returns a hashref representing the combined current header and meta information for the currently loaded database. She contains so much, let's go through it 🌸

The following fields are present in both version 1 and version 2 style databases (from the header):

enc_iv               => "123456789123456", # rand
enc_type             => "rijndael",
header_size          => 222,
seed_key             => "1234567890123456", # rand (32 bytes on v2)
seed_rand            => "12345678901234567890123456789012", # rand
rounds               => 6000,
sig1                 => "2594363651",
sig2                 => "3041655655", # indicates db version
ver                  => 196608,
version              => 1, # or 2

The following keys will be present after the reading of a version 2 database (from the header):

cipher               => "aes",
compression          => 1,
protected_stream     => "salsa20",
protected_stream_key => "12345678901234567890123456789012", # rand
start_bytes          => "12345678901234567890123456789012", # rand

Additionally, items parsed from the Meta section of a version 2 database will be added. The following are the available fields:

color                         => "#4FFF00",
custom_data                   => {key1 => "val1"},
database_description          => "database desc",
database_description_changed  => "2012-08-17 00:30:56",
database_name                 => "database name",
database_name_changed         => "2012-08-17 00:30:56",
default_user_name             => "",
default_user_name_changed     => "2012-08-17 00:30:34",
entry_templates_group         => "VL5nOpzlFUevGhqL71/OTA==",
entry_templates_group_changed => "2012-08-21 14:05:32",
generator                     => "KeePass",
history_max_items             => 10,
history_max_size              => 6291456, # bytes
last_selected_group           => "SUgL30QQqUK3tOWuNKUYJA==",
last_top_visible_group        => "dC1sQ1NO80W7klmRhfEUVw==",
maintenance_history_days      => 365,
master_key_change_force       => -1,
master_key_change_rec         => -1,
master_key_changed            => "2012-08-17 00:30:34",
protect_notes                 => 0,
protect_password              => 1,
protect_title                 => 0,
protect_url                   => 0,
protect_username              => 0
recycle_bin_changed           => "2012-08-17 00:30:34",
recycle_bin_enabled           => 1,
recycle_bin_uuid              => "SUgL30QQqUK3tOWuNKUYJA=="

When you're writing a database via either save_db or gen_db, these fields can be set and passed along 🌈 Optionally, it's possible to pass along a key called reuse_header to let calls to save_db and gen_db automatically use the contents of the previous header. Recycle, reduce, reuse πŸ’š

  • clear β€” clears any currently loaded database. Poof, gone, memory freed, we love a clean slate πŸŽ€
  • auto_lock β€” defaults to true. If true, passwords are automatically hidden when a database gets loaded via parse_db or load_db.
$k->auto_lock(0); # turn off auto locking
  • is_locked β€” returns true if the current database is locked. Vibe check on the lock status πŸ”’
  • lock β€” locks the database. This moves all passwords into a protected, in memory, encrypted storage location. Returns 1 on success. Returns 2 if the db is already locked. And if a database is loaded via parse_db or load_db and auto_lock is true, the newly loaded database will start out locked. Safety first, bestie πŸ›‘οΈ
  • unlock β€” unlocks a previously locked database. You will need to unlock a database before calling save_db or gen_db. No unlock, no save, that's the rule πŸ”“

(part 1 done, bestie β€” more incoming, don't go anywhere πŸ’–)

πŸ’– GROUP/ENTRY METHODS πŸ’–

okay bestie this is the part where we actually touch the data. groups, entries, finding them, deleting them β€” the whole social life of your database. let's go πŸ¦‹


🌸 dump_groups

gives you back a cute little simplified string version of whatever database is currently loaded. like a screenshot but for your KeePass vibes.

print $k->dump_groups;

you can optionally hand it a match-argument hashref, and then only the entries that match your criteria come back. picky, but make it fashion πŸ’…

πŸŽ€ groups

returns an arrayref of all the groups in the currently loaded database. and they come back hierarchal, as they should. important heads-up though: groups is literally just handing you a reference to all the data. it makes zero attempts at cleaning anything up. it's giving raw. (find_groups is the one that actually grooms the data, iykyk).

my $g = $k->groups;

the groups will look something like this:

$g = [{
     expanded => 0,
     icon     => 0,
     id       => 234234234, # under v1 this is a 32 bit int, under v2 it is a 16 char id
     title    => 'Foo',
     level    => 0,
     entries => [{
         accessed => "2010-06-24 15:09:19",
         comment  => "",
         created  => "2010-06-24 15:09:19",
         expires  => "2999-12-31 23:23:59",
         icon     => 0,
         modified => "2010-06-24 15:09:19",
         title    => "Something",
         password => 'somepass', # will be hidden if the database is locked
         url      => "",
         username => "someuser",
         id       => "0a55ac30af68149f", # v1 is any hex char, v2 is any 16 char
     }],
     groups => [{
         expanded => 0,
         icon     => 0,
         id       => 994414667,
         level    => 1,
         title    => "Bar"
     }],
 }];

✨ add_group

adds a brand new group to the database and returns a reference to it. if there's no database loaded yet, it just starts a fresh one, no drama. you pass a hashref of arguments for the new group β€” title, icon, expanded, all that. a new random group id gets generated for you. 🌈

there's also an optional group argument. pass one, and your shiny new group gets added under that parent group. nepotism but make it structural.

my $group = $k->add_group({title => 'Foo'});
my $gid = $group->{'id'};

my $group2 = $k->add_group({title => 'Bar', group => $gid});

oh and the group argument's value can also be a reference to a group itself β€” like the one you'd get back from find_group. flexible queen πŸ‘‘

🧚 finder_tests

this one is used by find_groups and find_entries. you give it a hashref of arguments, and it gives you back a list of test code refs. the little matchmakers of the whole operation.

{title => 'Foo'} # will check if title equals Foo
{'title !' => 'Foo'} # will check if title does not equal Foo
{'title =~' => qr{^Foo$}} # will check if title does matches the regex
{'title !~' => qr{^Foo$}} # will check if title does not match the regex

πŸ’œ find_groups

takes a hashref of search criteria and returns all the matching groups. you can pass id, title, icon, and level. the search arguments get parsed by finder_tests, obviously.

my @groups = $k->find_groups({title => 'Foo'});

my @all_groups_flattened = $k->find_groups({});

find_groups is also the responsible one β€” it double-checks that group ids are unique and that all the needed values are actually defined. main character energy with a to-do list. πŸ“‹

πŸ¦‹ find_group

calls find_groups and returns the first group it finds. and bestie, heads up: it dies if multiple results come up. it's not about that indecisive life.

in scalar context it returns just the group. in list context it returns the group and the arrayref where it's stored β€” either the root level group or a sub group's group item. 🌸

πŸ’… delete_group

passes your arguments to find_group to figure out which group you mean, then deletes it. returns the group that just got deleted. gone but never forgotten 🫢

πŸ”₯ add_entry

adds a new entry to the database and returns a reference to it. there's an optional group argument β€” and if you don't pass one, the entry lands in the first group in the database. no favourites here. a new entry id gets created if you don't pass one, or if the one you passed conflicts with an existing group. it sorts itself out.

these fields can be passed to both v1 and v2 databases:

accessed => "2010-06-24 15:09:19", # last accessed date
auto_type => [{keys => "{USERNAME}{TAB}{PASSWORD}{ENTER}", window => "Foo*"}],
binary   => {foo => 'content'}; # hashref of filename/content pairs
comment  => "", # a comment for the system - auto-type info is normally here
created  => "2010-06-24 15:09:19", # entry creation date
expires  => "2999-12-31 23:23:59", # date entry expires
icon     => 0, # icon number for use with agents
modified => "2010-06-24 15:09:19", # last modified
title    => "Something",
password => 'somepass', # will be hidden if the database is locked
url      => "http://",
username => "someuser",
id       => "0a55ac30af68149f", # auto generated if needed, v1 is any hex char, v2 is any 16 char
group    => $gid, # which group to add the entry to

for compatibility with earlier versions of File::KeePass, you can also pass in a binary and a binary_name when creating an entry. those get automatically converted into the hashref of filename/content pairs, cute little glow-up:

binary_name => "foo", # description of the stored binary - typically a filename
binary   => "content", # raw data to be stored in the system - typically a file

# results in
binary => {"foo" => "content"}

so like, typically, version 1 databases store their Auto-Type info inside the comment. they're also limited to only one key sequence per entry. not very demure of them. File::KeePass 2+ will automatically parse Auto-Type values passed in the entry comment and store them out as the auto_type arrayref. and that arrayref gets serialized right back into the comment section when saving as a version 1 database. version 2 databases have a whole separate storage mechanism for Auto-Type, because they're fancy like that. ✨

If you passed in:
comment => "
   Auto-Type: {USERNAME}{TAB}{PASSWORD}{ENTER}
   Auto-Type-Window: Foo*
   Auto-Type-Window: Bar*
",

Will result in:
auto_type => [{
    keys => "{USERNAME}{TAB}{PASSWORD}{ENTER}",
    window => "Foo*"
 }, {
    keys => "{USERNAME}{TAB}{PASSWORD}{ENTER}",
    window => "Bar*"
 }],

the group argument value can be either an existing group id, or a reference to a group β€” like the one find_group hands back. dealer's choice πŸ’–

when you're using a version 2 database, these additional fields are also available:

expires_enabled   => 0,
location_changed  => "2012-08-05 12:12:12",
usage_count       => 0,
tags              => {},
background_color  => '#ff0000',
foreground_color  => '#ffffff',
custom_icon_uuid  => '234242342aa',
history           => [], # arrayref of previous entry changes
override_url      => $node->{'OverrideURL'},
auto_type_enabled => 1,
auto_type_munge   => 0, # whether or not to attempt two channel auto typing
protected         => {password => 1}, # indicating which strings were/should be salsa20 protected
strings           => {'other key' => 'other value'},

⭐ find_entries

takes a hashref of search criteria and returns all matching groups. (yes bestie, the original says groups here, we're keeping it exactly as it is, no editing the source πŸ™ˆ). you can pass an entry id, title, username, comment, url, active, group_id, group_title, or literally any other entry property. the search arguments get parsed by finder_tests.

my @entries = $k->find_entries({title => 'Something'});

my @all_entries_flattened = $k->find_entries({});

πŸ“ find_entry

calls find_entries and returns the first entry found. dies if multiple results come up β€” it does not do ambiguity. in scalar context it returns just the entry. in list context it returns the entry and its group. two for one, slay. πŸ‘‘

🌈 delete_entry

passes arguments to find_entry to find the entry you want gone, then deletes it. returns the entry that was just deleted. rip to that entry fr fr πŸ’€

🧚 locked_entry_password

this one lets you access individual passwords for a database that is locked. and yes β€” it dies if the database is not locked. it only works when it's locked, that's the whole bit. πŸ”’


✨ UTILITY METHODS ✨

Okay bestie, these are the general purpose little helpers that do the heavy lifting while kdb databases are being parsed and generated. Think of them as the backstage crew. Not the main characters, but the show does not go on without them. πŸŽ€

  • now β€” hands you the current localtime datetime stamp. clock check, slay. πŸ•
  • default_exp β€” returns the string that represents an entry's default expires time. It uses $self->{'default_exp'}, or if that's not around it falls back (the docs literally say "fails", and honestly same, bestie 😭) to the string '2999-12-31 23:23:59'.
  • decrypt_rijndael_cbc β€” you give it an encrypted string, a key, and an encryption_iv string. It gives you back a plaintext string. unlocking, but make it cryptography. πŸ”“
  • encrypt_rijndael_cbc β€” the reverse glow-up: takes a plaintext string, a key, and an encryption_iv string, and returns an encrypted string.
  • decode_base64 β€” loads the MIME::Base64 library and decodes whatever string you pass it.
  • encode_base64 β€” loads the MIME::Base64 library and encodes whatever string you pass it.
  • unchunksum β€” parses and reassembles a buffer, reading in the lengths and the checksums of the chunks. putting the puzzle back together fr fr. 🧩
  • decompress β€” loads the Compress::Raw::Zlib library and inflates the contents.
  • compress β€” loads the Compress::Raw::Zlib library and deflates the contents.
  • parse_xml β€” loads the XML::Parser library and sets up a basic parser that can call hooks at various events. Without the hooks, it runs similarly to XML::Simple::parse. vibe check incoming:
my $data = $self->parse_xml($buffer, {
    top            => 'KeePassFile',
    force_array    => {Group => 1, Entry => 1},
    start_handlers => {Group => sub { $level++ }},
    end_handlers   => {Group => sub { $level-- }},
});
  • gen_xml β€” generates XML from the data structure you pass in. The output of parse_xml can be passed as is. And bonus: you can drop in hints like __sort__ to order the tags of a node, and __attr__ to say which items of a node are attributes. icon behavior. πŸ‘‘
  • salsa20 β€” takes a hashref containing a salsa20 key string (length 32 or 16), a salsa20 iv string (length 8), a number of salsa20 rounds (8, 12, or 20 β€” default 20), and an optional data string. The key and iv get used to initialize the salsa20 encryption.

If a data string is passed, that string is salsa20 encrypted and returned.

If no data string is passed, a salsa20 encrypting coderef is returned instead. cute, right? πŸ’«

my $encoded = $self->salsa20({key => $key, iv => $iv, data => $data});
my $uncoded = $self->salsa20({key => $key, iv => $iv, data => $encoded});
# $data eq $uncoded

my $encoder = $self->salsa20({key => $key, iv => $Iv}); # no data
my $encoded = $encoder->($data);
my $part2   = $encoder->($more_data); # continues from previous state
  • salsa20_stream β€” takes a hashref that will be passed straight to salsa20. Then it uses the resulting encoder to generate a more continuous encoded stream. Here's the tea: the salsa20 method encodes in chunks of 64 bytes, so if a string isn't a multiple of 64, some of the xor bytes just sit there unused. salsa20_stream maintains a buffer of xor bytes so none of them get wasted. no byte left behind. 🫢
my $encoder = $self->salsa20_stream({key => $key, iv => $Iv}); # no data
my $encoded = $encoder->("1234");   # calls salsa20->()
my $part2   = $encoder->("1234");   # uses the same pad until 64 bytes are used

πŸ”’ OTHER METHODS πŸ”’

These are the little undercover agents β€” they start with an underscore, they mind their business, they do the work. πŸ™ˆ

  • _parse_v1_header
  • _parse_v1_body
  • _parse_v1_groups
  • _parse_v1_entries
  • _parse_v1_date

These are the utilities used for parsing version 1 type databases.

  • _parse_v2_header
  • _parse_v2_body
  • _parse_v2_date

And these are the utilities used for parsing version 2 type databases.

  • _gen_v1_db
  • _gen_v1_header
  • _gen_v1_date

Utilities used to generate version 1 type databases.

  • _gen_v2_db
  • _gen_v2_header
  • _gen_v2_date

Utilities used to generate version 2 type databases.

  • _master_key β€” takes the password and the parsed headers, and returns the master key based on the database type. the master key. the main character. πŸ’…

🌈 ONE LINERS 🌈

(Long one liners)

Here's a version 1 to version 2 converter, and also a version 2 to version 1 converter, because it goes both ways, periodt. Simply change the extension of the two files. Someday we will include a kdb2kdbx utility to do this for you, but for now we're doing it live.

perl -MFile::KeePass -e 'use IO::Prompt; $p="".prompt("Pass:",-e=>"*",-tty); File::KeePass->load_db(+shift,$p,{auto_lock=>0})->save_db(+shift,$p)' ~/test.kdb ~/test.kdbx

# OR using graphical prompt
perl -MFile::KeePass -e 'chop($p=`zenity --password`); File::KeePass->load_db(+shift,$p,{auto_lock=>0})->save_db(+shift,$p)' ~/test.kdbx ~/test.kdb

# OR using pure perl (but echoes password)
perl -MFile::KeePass -e 'print "Pass:"; chop($p=<STDIN>); File::KeePass->load_db(+shift,$p,{auto_lock=>0})->save_db(+shift,$p)' ~/test.kdbx ~/test.kdb
Heads up bestie: that pure perl one echoes your password. 😭

Dumping the XML from a version 2 database:

perl -MFile::KeePass -e 'chop($p=`zenity --password`); print File::KeePass->load_db(+shift,$p,{keep_xml=>1})->{xml_in},"\n"' ~/test.kdbx

Outlining group information:

perl -MFile::KeePass -e 'chop($p=`zenity --password`); print File::KeePass->load_db(+shift,$p)->dump_groups' ~/test.kdbx

Dumping header information:

perl -MFile::KeePass -MData::Dumper -e 'chop($p=`zenity --password`); print Dumper +File::KeePass->load_db(+shift,$p)->header' ~/test.kdbx

πŸ’€ BUGS πŸ’€

  • Only Rijndael is supported when using v1 databases.
  • This module makes no attempt to act as a password agent. That is the job of File::KeePass::Agent. This isn't really a bug but some people will think it is. (and honestly, they will. 😭)
  • Groups and entries don't have true objects associated with them. At the moment this is by design. The data is kept as plain boring data. lowkey iconic for keeping it simple ngl.

πŸ“š SOURCES πŸ“š

The knowledge about the algorithms necessary to decode a KeePass DB v1 format was gleaned from the source code of keepassx-0.4.3. That source code is published under the GPL2 license. KeePassX 0.4.3 bears the copyright of

Copyright (C) 2005-2008 Tarek Saidi <tarek.saidi@arcor.de>
Copyright (C) 2007-2009 Felix Geyer <debfx-keepassx {at} fobos.de>

The knowledge about the algorithms necessary to decode a KeePass DB v2 format was gleaned from the source code of keepassx-2.0-alpha1. That source code is published under the GPL2 or GPL3 license. KeePassX 2.0-alpha1 bears the copyright of

Copyright: 2010-2012, Felix Geyer <debfx@fobos.de>
           2011-2012, Florian Geyer <blueice@fobos.de>

The salsa20 algorithm is based on http://cr.yp.to/snuffle/salsa20/regs/salsa20.c which is listed as Public domain (D. J. Bernstein).

The ordering and layering of encryption/decryption algorithms of File::KeePass are of derivative nature from KeePassX and could not have been created without this insight β€” though the perl code is from scratch. ✨


πŸ‘‘ AUTHOR πŸ‘‘

Paul Seamons <paul@seamons.com>


πŸ’– LICENSE πŸ’–

This module may be distributed under the same terms as Perl itself.


and that's a wrap, bestie β€” this was File::KeePass(3pm) from perl v5.32.0, dated 2021-01-02. go forth and keep your passwords safe, ily 🫢🌸

πŸ“¦ where this page comes from