Start LibreLedger: an encrypted money ledger that stores only ciphertext
LibreLedger is a single-person money ledger. The browser encrypts
everything (PBKDF2-HMAC-SHA256 with 250k iterations, then AES-256-GCM),
and a small standard-library Python server stores only the resulting
{v, salt, iv, ct} blob. Ledger files from the earlier Money Ledger version
open unchanged.
- On plain http to a LAN or VPN address, where browsers hide Web Crypto,
the app switches to vendored @noble/hashes and @noble/ciphers 2.2.0 and
says so on the lock screen. tests/crypto-interop.test.mjs shows both
paths read each other's files.
- The server hands out only the app's own files, sends a self-only CSP,
nosniff, frame denial and no-referrer, checks the shape of each blob,
refuses cross-site writes and writes atomically. It keeps the last 10
backups plus one per day for 30 days.
- The container runs that server from a digest-pinned python alpine
image, as an unprivileged user, with its data in /data and a health
check on /api/health.
Assisted-by: Claude Opus 5 <noreply@anthropic.com>
@@ -0,0 +1,10 @@
|
|||||||
|
*
|
||||||
|
!server.py
|
||||||
|
!index.html
|
||||||
|
!app.js
|
||||||
|
!theme.js
|
||||||
|
!crypto-fallback.js
|
||||||
|
!styles.css
|
||||||
|
!favicon.svg
|
||||||
|
!banks/*.svg
|
||||||
|
!vendor/**
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Encrypted ledger data: never commit it, even though it is encrypted
|
||||||
|
data/
|
||||||
|
*.enc
|
||||||
|
*.enc.bak
|
||||||
|
*.enc.*
|
||||||
|
|
||||||
|
# Local design-reference screenshots may contain real figures
|
||||||
|
preview.png
|
||||||
|
|
||||||
|
# Local Claude Code settings
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Test output and caches
|
||||||
|
tests/out/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# OS / editor cruft
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.swp
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# LibreLedger — the app plus its persistence server (server.py, standard
|
||||||
|
# library only). The browser encrypts; the container only stores ciphertext.
|
||||||
|
#
|
||||||
|
# Build: docker build -t libreledger .
|
||||||
|
# Run: docker run --rm -p 8080:8080 -v libreledger-data:/data libreledger
|
||||||
|
# Open: http://localhost:8080
|
||||||
|
#
|
||||||
|
# Base image pinned by version AND index digest (multi-arch). Bump both together.
|
||||||
|
FROM python:3.14.7-alpine3.24@sha256:c6ead215bfd31f1e433d968853b7a769989117115b728874824e6c0a27cb96fc
|
||||||
|
|
||||||
|
ARG VERSION=1.0.0
|
||||||
|
LABEL org.opencontainers.image.title="LibreLedger" \
|
||||||
|
org.opencontainers.image.description="Encrypted personal money ledger: your data is encrypted in your browser; the server never sees it." \
|
||||||
|
org.opencontainers.image.version="${VERSION}" \
|
||||||
|
org.opencontainers.image.source="https://git.libreportal.org/LibrePortal/LibreLedger" \
|
||||||
|
org.opencontainers.image.licenses="AGPL-3.0-only"
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
LIBRELEDGER_HOST=0.0.0.0 \
|
||||||
|
LIBRELEDGER_PORT=8080 \
|
||||||
|
LIBRELEDGER_DATA_DIR=/data
|
||||||
|
|
||||||
|
# Unprivileged by default. LibrePortal may override the uid so the process
|
||||||
|
# owns its bind mount (under rootless Docker that is container root, which is
|
||||||
|
# the unprivileged install user on the host).
|
||||||
|
RUN addgroup -S -g 10001 libreledger \
|
||||||
|
&& adduser -S -D -H -u 10001 -G libreledger -s /sbin/nologin libreledger \
|
||||||
|
&& mkdir -p /data \
|
||||||
|
&& chown libreledger:libreledger /data \
|
||||||
|
&& chmod 700 /data
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --chmod=0444 server.py index.html app.js theme.js crypto-fallback.js styles.css favicon.svg ./
|
||||||
|
COPY --chmod=0444 banks/*.svg ./banks/
|
||||||
|
COPY --chmod=0444 vendor/ ./vendor/
|
||||||
|
RUN find /app -type d -exec chmod 0555 {} +
|
||||||
|
|
||||||
|
USER libreledger
|
||||||
|
EXPOSE 8080
|
||||||
|
VOLUME ["/data"]
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||||
|
CMD ["python3", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/api/health', timeout=4).status == 200 else 1)"]
|
||||||
|
CMD ["python3", "/app/server.py"]
|
||||||
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# LibreLedger
|
||||||
|
|
||||||
|
An encrypted personal money ledger: **your data is encrypted in your browser;
|
||||||
|
the server never sees it.**
|
||||||
|
|
||||||
|
One screen, one continuous running balance:
|
||||||
|
|
||||||
|
- **Balances** at the top: your accounts and their balances, with a grand total. That total is where the ledger *opens*.
|
||||||
|
- **Monthly ledger** below: one block per month, a table of `Date · Type · Description · In · Out · Balance`. The balance runs on from your account balances and **carries from each month into the next**.
|
||||||
|
- **Type** comes from the row: money *in* shows a green **Money** badge, money *out* an amber **Cost** badge.
|
||||||
|
- Each row has a **category** (emoji and colour) for tagging entries at a glance.
|
||||||
|
- Insert or remove rows anywhere, and add or remove whole months.
|
||||||
|
- **Recurring** bills and income (rent, phone, salary…), each with an amount, a category and a schedule:
|
||||||
|
- **Monthly** on a chosen day, **every N weeks** from an anchor date, or **yearly** on a month and day.
|
||||||
|
- **🔁 Add recurring** drops every matching occurrence into a month as real, editable rows; **+ Add month** moves to the next calendar month and pre-fills it.
|
||||||
|
- **Totals**, **Savings** and **Housing affordability** summaries, several **budgets** in one ledger, CSV and PDF export.
|
||||||
|
- **Dark / light theme**, dark by default.
|
||||||
|
|
||||||
|
## Zero-knowledge encryption
|
||||||
|
|
||||||
|
- Everything is encrypted **in the browser** with **AES-256-GCM**. The key is
|
||||||
|
derived from your passphrase with **PBKDF2-HMAC-SHA256, 250,000 iterations**
|
||||||
|
and a random 16-byte salt; each save uses a fresh random 12-byte IV.
|
||||||
|
- The server (`server.py`) only stores and returns the resulting blob,
|
||||||
|
`{ "v": 1, "salt": …, "iv": …, "ct": … }` (base64). It never receives the
|
||||||
|
passphrase, the key or any figure from the ledger.
|
||||||
|
- There is no account and no recovery. **Lose the passphrase and the data is
|
||||||
|
gone**; that is the point.
|
||||||
|
- The page loads nothing from anywhere else. The server sends a strict
|
||||||
|
Content-Security-Policy (`'self'` only), `nosniff`, frame denial and
|
||||||
|
`no-referrer`, and serves only the app's own files.
|
||||||
|
|
||||||
|
### Plain http on your LAN or VPN
|
||||||
|
|
||||||
|
Browsers only give pages their built-in encryption (Web Crypto) in a *secure
|
||||||
|
context*: HTTPS or `http://localhost`. Opened as `http://192.168.x.x:port` or
|
||||||
|
over a VPN address, LibreLedger switches to its **built-in fallback**: the same
|
||||||
|
PBKDF2-SHA256 and AES-256-GCM from the audited
|
||||||
|
[@noble/hashes](https://github.com/paulmillr/noble-hashes) and
|
||||||
|
[@noble/ciphers](https://github.com/paulmillr/noble-ciphers) libraries,
|
||||||
|
vendored and pinned in [`vendor/`](vendor/README.md). The files it writes are
|
||||||
|
identical in format, so a ledger saved one way opens the other way.
|
||||||
|
|
||||||
|
The lock screen says when the fallback is in use. Unlocking takes a few seconds
|
||||||
|
longer, and the key sits in page memory rather than in the browser's key store,
|
||||||
|
so **prefer HTTPS** whenever you have it. Plain http also means anyone on the
|
||||||
|
network path can see (encrypted) traffic and could tamper with the page itself,
|
||||||
|
so only use it on a network you trust.
|
||||||
|
|
||||||
|
## Self-hosting
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name libreledger -p 127.0.0.1:8080:8080 \
|
||||||
|
-v libreledger-data:/data \
|
||||||
|
git.libreportal.org/libreportal/libreledger:1.0.0
|
||||||
|
# open http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Or build it yourself: `docker build -t libreledger .`
|
||||||
|
|
||||||
|
The image runs `server.py` on port 8080 as an unprivileged user, keeps its data
|
||||||
|
in the `/data` volume and has a health check on `/api/health`. Put HTTPS in
|
||||||
|
front of it (Caddy, Traefik, nginx…) when it is reachable from anywhere but
|
||||||
|
your own machine.
|
||||||
|
|
||||||
|
### Without Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 server.py # http://127.0.0.1:8080, data in ./data
|
||||||
|
python3 server.py --host 0.0.0.0 --port 8080 --data-dir /srv/libreledger
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 3.9 or newer, standard library only.
|
||||||
|
|
||||||
|
| Option | Environment | Default |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `--host` | `LIBRELEDGER_HOST` | `127.0.0.1` |
|
||||||
|
| `--port` | `LIBRELEDGER_PORT` | `8080` |
|
||||||
|
| `--data-dir` | `LIBRELEDGER_DATA_DIR` | `./data` |
|
||||||
|
| `--backup-interval` | `LIBRELEDGER_BACKUP_INTERVAL` | `600` seconds (`0`: every save) |
|
||||||
|
| `--backup-keep-recent` | `LIBRELEDGER_BACKUP_KEEP_RECENT` | `10` |
|
||||||
|
| `--backup-keep-daily` | `LIBRELEDGER_BACKUP_KEEP_DAILY` | `30` |
|
||||||
|
|
||||||
|
### Your data
|
||||||
|
|
||||||
|
- `<data-dir>/ledger.enc` is the ledger, written atomically on every change.
|
||||||
|
- `<data-dir>/backups/ledger-<UTC time>.enc` are earlier versions: the newest
|
||||||
|
10, plus the last one of each of the most recent 30 days. At most one is taken
|
||||||
|
per backup interval.
|
||||||
|
- All of them are encrypted with the passphrase that was current when they were
|
||||||
|
written. To restore one, stop the server and copy it over `ledger.enc`, or
|
||||||
|
use **Restore** in the app.
|
||||||
|
- To move a ledger to another server, copy `ledger.enc` into its data folder
|
||||||
|
and unlock it with your passphrase. Files from the earlier "Money Ledger"
|
||||||
|
version of this app work as they are.
|
||||||
|
|
||||||
|
The server has no login of its own: anyone who can reach it can fetch the
|
||||||
|
encrypted file or overwrite it (the backups cover the latter). Keep it on a
|
||||||
|
private network, a VPN, or behind an authenticating proxy.
|
||||||
|
|
||||||
|
### Install on LibrePortal
|
||||||
|
|
||||||
|
LibreLedger is in the [LibrePortal](https://git.libreportal.org/LibrePortal/LibrePortal)
|
||||||
|
app catalog: `libreportal app install libreledger`, or **Apps → LibreLedger**
|
||||||
|
in the WebUI. It installs *private* (reachable only from your trusted places),
|
||||||
|
with its data included in LibrePortal's backups, and on
|
||||||
|
`https://ledger.<your domain>` when a domain is set up.
|
||||||
|
|
||||||
|
## In the app
|
||||||
|
|
||||||
|
- **💾 Saved to disk**: the encrypted ledger is saving to the server.
|
||||||
|
- **🔗 Link file** *(Chrome/Edge desktop, when opened without the server)*:
|
||||||
|
auto-save to an encrypted file you choose.
|
||||||
|
- **Export CSV / PDF**: plaintext exports of the decrypted data. Handle them with care.
|
||||||
|
- **Backup / Restore**: download or load an *encrypted* copy.
|
||||||
|
- **Change passphrase**: re-encrypts with a new passphrase. Older backups keep the old one.
|
||||||
|
- **Lock**: forgets the key and the data; the passphrase is needed again.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tests/test_server.py # server: allowlist, headers, checks, backups
|
||||||
|
node tests/crypto-interop.test.mjs # Web Crypto <-> fallback interop (Node 22.12+)
|
||||||
|
```
|
||||||
|
|
||||||
|
Files: `index.html` (markup and lock screen), `styles.css`, `app.js` (state,
|
||||||
|
rendering and the crypto layer), `theme.js`, `crypto-fallback.js`, `server.py`,
|
||||||
|
`banks/` (logos, see `banks/NOTICE.txt`), `vendor/`.
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
[GNU AGPLv3](LICENSE). The vendored noble libraries are MIT-licensed (their
|
||||||
|
`LICENSE` files are next to them); bank logos are trademarks of their owners
|
||||||
|
(`banks/NOTICE.txt`).
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Bank/brand logos in this folder are trademarks of their respective owners and are
|
||||||
|
bundled here only to help you visually identify your own accounts in this personal,
|
||||||
|
offline tool (nominative use). Sources: Simple Icons (CC0) and Wikimedia Commons
|
||||||
|
(freely-licensed files). No affiliation or endorsement is implied. Banks whose logos
|
||||||
|
are not freely licensed (e.g. NatWest, Halifax, Nationwide) use a colour badge instead.
|
||||||
|
Remove any you don't want.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="#2E77BC" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>American Express</title><path d="M16.015 14.378c0-.32-.135-.496-.344-.622-.21-.12-.464-.135-.81-.135h-1.543v2.82h.675v-1.027h.72c.24 0 .39.024.478.125.12.13.104.38.104.55v.35h.66v-.555c-.002-.25-.017-.376-.108-.516-.06-.08-.18-.18-.33-.234l.02-.008c.18-.072.48-.297.48-.747zm-.87.407l-.028-.002c-.09.053-.195.058-.33.058h-.81v-.63h.824c.12 0 .24 0 .33.05.098.048.156.147.15.255 0 .12-.045.215-.134.27zM20.297 15.837H19v.6h1.304c.676 0 1.05-.278 1.05-.884 0-.28-.066-.448-.187-.582-.153-.133-.392-.193-.73-.207l-.376-.015c-.104 0-.18 0-.255-.03-.09-.03-.15-.105-.15-.21 0-.09.017-.166.09-.21.083-.046.177-.066.272-.06h1.23v-.602h-1.35c-.704 0-.958.437-.958.84 0 .9.776.855 1.407.87.104 0 .18.015.225.06.046.03.082.106.082.18 0 .077-.035.15-.08.18-.06.053-.15.07-.277.07zM0 0v10.096L.81 8.22h1.75l.225.464V8.22h2.043l.45 1.02.437-1.013h6.502c.295 0 .56.057.756.236v-.23h1.787v.23c.307-.17.686-.23 1.12-.23h2.606l.24.466v-.466h1.918l.254.465v-.466h1.858v3.948H20.87l-.36-.6v.585h-2.353l-.256-.63h-.583l-.27.614h-1.213c-.48 0-.84-.104-1.08-.24v.24h-2.89v-.884c0-.12-.03-.12-.105-.135h-.105v1.036H6.067v-.48l-.21.48H4.69l-.202-.48v.465H2.235l-.256-.624H1.4l-.256.624H0V24h23.786v-7.108c-.27.135-.613.18-.973.18H21.09v-.255c-.21.165-.57.255-.914.255H14.71v-.9c0-.12-.018-.12-.12-.12h-.075v1.022h-1.8v-1.066c-.298.136-.643.15-.928.136h-.214v.915h-2.18l-.54-.617-.57.6H4.742v-3.93h3.61l.518.602.554-.6h2.412c.28 0 .74.03.942.225v-.24h2.177c.202 0 .644.045.903.225v-.24h3.265v.24c.163-.164.508-.24.803-.24h1.89v.24c.194-.15.464-.24.84-.24h1.176V0H0zM21.156 14.955c.004.005.006.012.01.016.01.01.024.01.032.02l-.042-.035zM23.828 13.082h.065v.555h-.065zM23.865 15.03v-.005c-.03-.025-.046-.048-.075-.07-.15-.153-.39-.215-.764-.225l-.36-.012c-.12 0-.194-.007-.27-.03-.09-.03-.15-.105-.15-.21 0-.09.03-.16.09-.204.076-.045.15-.05.27-.05h1.223v-.588h-1.283c-.69 0-.96.437-.96.84 0 .9.78.855 1.41.87.104 0 .18.015.224.06.046.03.076.106.076.18 0 .07-.034.138-.09.18-.045.056-.136.07-.27.07h-1.288v.605h1.287c.42 0 .734-.118.9-.36h.03c.09-.134.135-.3.135-.523 0-.24-.045-.39-.135-.526zM18.597 14.208v-.583h-2.235V16.458h2.235v-.585h-1.57v-.57h1.533v-.584h-1.532v-.51M13.51 8.787h.685V11.6h-.684zM13.126 9.543l-.007.006c0-.314-.13-.5-.34-.624-.217-.125-.47-.135-.81-.135H10.43v2.82h.674v-1.034h.72c.24 0 .39.03.487.12.122.136.107.378.107.548v.354h.677v-.553c0-.25-.016-.375-.11-.516-.09-.107-.202-.19-.33-.237.172-.07.472-.3.472-.75zm-.855.396h-.015c-.09.054-.195.056-.33.056H11.1v-.623h.825c.12 0 .24.004.33.05.09.04.15.128.15.25s-.047.22-.134.266zM15.92 9.373h.632v-.6h-.644c-.464 0-.804.105-1.02.33-.286.3-.362.69-.362 1.11 0 .512.123.833.36 1.074.232.238.645.31.97.31h.78l.255-.627h1.39l.262.627h1.36v-2.11l1.272 2.11h.95l.002.002V8.786h-.684v1.963l-1.18-1.96h-1.02V11.4L18.11 8.744h-1.004l-.943 2.22h-.3c-.177 0-.362-.03-.468-.134-.125-.15-.186-.36-.186-.662 0-.285.08-.51.194-.63.133-.135.272-.165.516-.165zm1.668-.108l.464 1.118v.002h-.93l.466-1.12zM2.38 10.97l.254.628H4V9.393l.972 2.205h.584l.973-2.202.015 2.202h.69v-2.81H6.118l-.807 1.904-.876-1.905H3.343v2.663L2.205 8.787h-.997L.01 11.597h.72l.26-.626h1.39zm-.688-1.705l.46 1.118-.003.002h-.915l.457-1.12zM11.856 13.62H9.714l-.85.923-.825-.922H5.346v2.82H8l.855-.932.824.93h1.302v-.94h.838c.6 0 1.17-.164 1.17-.945l-.006-.003c0-.78-.598-.93-1.128-.93zM7.67 15.853l-.014-.002H6.02v-.557h1.47v-.574H6.02v-.51H7.7l.733.82-.764.824zm2.642.33l-1.03-1.147 1.03-1.108v2.253zm1.553-1.258h-.885v-.717h.885c.24 0 .42.098.42.344 0 .243-.15.372-.42.372zM9.967 9.373v-.586H7.73V11.6h2.237v-.58H8.4v-.564h1.527V9.88H8.4v-.507"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,637 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.0"
|
||||||
|
width="214.66"
|
||||||
|
height="16.629999"
|
||||||
|
id="svg2"
|
||||||
|
xml:space="preserve"><defs
|
||||||
|
id="defs5"><clipPath
|
||||||
|
id="clipPath17"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path19" /></clipPath><clipPath
|
||||||
|
id="clipPath55"><path
|
||||||
|
d="M 901.588,604.63 C 901.588,604.63 894.898,604.63 894.898,597.94 L 894.898,597.94 L 894.898,572.201 C 894.898,572.201 894.898,565.511 901.588,565.511 L 901.588,565.511 L 979.37,565.511 C 979.37,565.511 986.06,565.511 986.06,572.201 L 986.06,572.201 L 986.06,597.94 C 986.06,597.94 986.06,604.63 979.37,604.63 L 979.37,604.63 L 901.588,604.63 z"
|
||||||
|
id="path57" /></clipPath><clipPath
|
||||||
|
id="clipPath67"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path69" /></clipPath><clipPath
|
||||||
|
id="clipPath85"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path87" /></clipPath><clipPath
|
||||||
|
id="clipPath97"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path99" /></clipPath><clipPath
|
||||||
|
id="clipPath109"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path111" /></clipPath><clipPath
|
||||||
|
id="clipPath121"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path123" /></clipPath><clipPath
|
||||||
|
id="clipPath133"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path135" /></clipPath><clipPath
|
||||||
|
id="clipPath145"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path147" /></clipPath><clipPath
|
||||||
|
id="clipPath157"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path159" /></clipPath><clipPath
|
||||||
|
id="clipPath169"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path171" /></clipPath><clipPath
|
||||||
|
id="clipPath181"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path183" /></clipPath><clipPath
|
||||||
|
id="clipPath193"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path195" /></clipPath><clipPath
|
||||||
|
id="clipPath205"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path207" /></clipPath><clipPath
|
||||||
|
id="clipPath217"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path219" /></clipPath><clipPath
|
||||||
|
id="clipPath229"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path231" /></clipPath><clipPath
|
||||||
|
id="clipPath241"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path243" /></clipPath><clipPath
|
||||||
|
id="clipPath253"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path255" /></clipPath><clipPath
|
||||||
|
id="clipPath265"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path267" /></clipPath><clipPath
|
||||||
|
id="clipPath277"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path279" /></clipPath><clipPath
|
||||||
|
id="clipPath289"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path291" /></clipPath><clipPath
|
||||||
|
id="clipPath301"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path303" /></clipPath><clipPath
|
||||||
|
id="clipPath313"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path315" /></clipPath><clipPath
|
||||||
|
id="clipPath325"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path327" /></clipPath><clipPath
|
||||||
|
id="clipPath337"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path339" /></clipPath><clipPath
|
||||||
|
id="clipPath349"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path351" /></clipPath><clipPath
|
||||||
|
id="clipPath361"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path363" /></clipPath><clipPath
|
||||||
|
id="clipPath373"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path375" /></clipPath><clipPath
|
||||||
|
id="clipPath385"><path
|
||||||
|
d="M 573,55.525 L 850.795,55.525 L 850.795,84.581 L 573,84.581 L 573,55.525 z"
|
||||||
|
id="path387" /></clipPath><clipPath
|
||||||
|
id="clipPath431"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path433" /></clipPath><clipPath
|
||||||
|
id="clipPath443"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path445" /></clipPath><clipPath
|
||||||
|
id="clipPath455"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path457" /></clipPath><clipPath
|
||||||
|
id="clipPath467"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path469" /></clipPath><clipPath
|
||||||
|
id="clipPath479"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path481" /></clipPath><clipPath
|
||||||
|
id="clipPath491"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path493" /></clipPath><clipPath
|
||||||
|
id="clipPath503"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path505" /></clipPath><clipPath
|
||||||
|
id="clipPath515"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path517" /></clipPath><clipPath
|
||||||
|
id="clipPath527"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path529" /></clipPath><clipPath
|
||||||
|
id="clipPath539"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path541" /></clipPath><clipPath
|
||||||
|
id="clipPath551"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path553" /></clipPath><clipPath
|
||||||
|
id="clipPath563"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path565" /></clipPath><clipPath
|
||||||
|
id="clipPath575"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path577" /></clipPath><clipPath
|
||||||
|
id="clipPath587"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path589" /></clipPath><clipPath
|
||||||
|
id="clipPath599"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path601" /></clipPath><clipPath
|
||||||
|
id="clipPath611"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path613" /></clipPath><clipPath
|
||||||
|
id="clipPath623"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path625" /></clipPath><clipPath
|
||||||
|
id="clipPath635"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path637" /></clipPath><clipPath
|
||||||
|
id="clipPath647"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path649" /></clipPath><clipPath
|
||||||
|
id="clipPath659"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path661" /></clipPath><clipPath
|
||||||
|
id="clipPath671"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path673" /></clipPath><clipPath
|
||||||
|
id="clipPath683"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path685" /></clipPath><clipPath
|
||||||
|
id="clipPath695"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path697" /></clipPath><clipPath
|
||||||
|
id="clipPath707"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path709" /></clipPath><clipPath
|
||||||
|
id="clipPath719"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path721" /></clipPath><clipPath
|
||||||
|
id="clipPath731"><path
|
||||||
|
d="M 992.527,59.953 L 1270.323,59.953 L 1270.323,109.1 L 992.527,109.1 L 992.527,59.953 z"
|
||||||
|
id="path733" /></clipPath><clipPath
|
||||||
|
id="clipPath771"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path773" /></clipPath><clipPath
|
||||||
|
id="clipPath1069"><path
|
||||||
|
d="M 752.449,97.795 L 850.245,97.795 L 850.245,142.158 L 752.449,142.158 L 752.449,97.795 z"
|
||||||
|
id="path1071" /></clipPath><clipPath
|
||||||
|
id="clipPath1081"><path
|
||||||
|
d="M 752.449,97.795 L 850.245,97.795 L 850.245,142.158 L 752.449,142.158 L 752.449,97.795 z"
|
||||||
|
id="path1083" /></clipPath><clipPath
|
||||||
|
id="clipPath1093"><path
|
||||||
|
d="M 752.449,97.795 L 850.245,97.795 L 850.245,142.158 L 752.449,142.158 L 752.449,97.795 z"
|
||||||
|
id="path1095" /></clipPath><clipPath
|
||||||
|
id="clipPath1105"><path
|
||||||
|
d="M 752.449,97.795 L 850.245,97.795 L 850.245,142.158 L 752.449,142.158 L 752.449,97.795 z"
|
||||||
|
id="path1107" /></clipPath><clipPath
|
||||||
|
id="clipPath1117"><path
|
||||||
|
d="M 752.449,97.795 L 850.245,97.795 L 850.245,142.158 L 752.449,142.158 L 752.449,97.795 z"
|
||||||
|
id="path1119" /></clipPath><clipPath
|
||||||
|
id="clipPath1129"><path
|
||||||
|
d="M 752.449,97.795 L 850.245,97.795 L 850.245,142.158 L 752.449,142.158 L 752.449,97.795 z"
|
||||||
|
id="path1131" /></clipPath><clipPath
|
||||||
|
id="clipPath1141"><path
|
||||||
|
d="M 752.449,97.795 L 850.245,97.795 L 850.245,142.158 L 752.449,142.158 L 752.449,97.795 z"
|
||||||
|
id="path1143" /></clipPath><clipPath
|
||||||
|
id="clipPath1153"><path
|
||||||
|
d="M 752.449,97.795 L 850.245,97.795 L 850.245,142.158 L 752.449,142.158 L 752.449,97.795 z"
|
||||||
|
id="path1155" /></clipPath><clipPath
|
||||||
|
id="clipPath1165"><path
|
||||||
|
d="M 752.449,97.795 L 850.245,97.795 L 850.245,142.158 L 752.449,142.158 L 752.449,97.795 z"
|
||||||
|
id="path1167" /></clipPath><clipPath
|
||||||
|
id="clipPath1177"><path
|
||||||
|
d="M 752.449,97.795 L 850.245,97.795 L 850.245,142.158 L 752.449,142.158 L 752.449,97.795 z"
|
||||||
|
id="path1179" /></clipPath><clipPath
|
||||||
|
id="clipPath1189"><path
|
||||||
|
d="M 752.449,97.795 L 850.245,97.795 L 850.245,142.158 L 752.449,142.158 L 752.449,97.795 z"
|
||||||
|
id="path1191" /></clipPath><clipPath
|
||||||
|
id="clipPath1201"><path
|
||||||
|
d="M 752.449,97.795 L 850.245,97.795 L 850.245,142.158 L 752.449,142.158 L 752.449,97.795 z"
|
||||||
|
id="path1203" /></clipPath><clipPath
|
||||||
|
id="clipPath1213"><path
|
||||||
|
d="M 752.449,97.795 L 850.245,97.795 L 850.245,142.158 L 752.449,142.158 L 752.449,97.795 z"
|
||||||
|
id="path1215" /></clipPath><clipPath
|
||||||
|
id="clipPath1359"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1361" /></clipPath><clipPath
|
||||||
|
id="clipPath1397"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1399" /></clipPath><clipPath
|
||||||
|
id="clipPath1411"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1413" /></clipPath><clipPath
|
||||||
|
id="clipPath1773"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1775" /></clipPath><clipPath
|
||||||
|
id="clipPath1785"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1787" /></clipPath><clipPath
|
||||||
|
id="clipPath1797"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1799" /></clipPath><clipPath
|
||||||
|
id="clipPath1809"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1811" /></clipPath><clipPath
|
||||||
|
id="clipPath1821"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1823" /></clipPath><clipPath
|
||||||
|
id="clipPath1833"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1835" /></clipPath><clipPath
|
||||||
|
id="clipPath1845"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1847" /></clipPath><clipPath
|
||||||
|
id="clipPath1857"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1859" /></clipPath><clipPath
|
||||||
|
id="clipPath1869"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1871" /></clipPath><clipPath
|
||||||
|
id="clipPath1881"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1883" /></clipPath><clipPath
|
||||||
|
id="clipPath1893"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1895" /></clipPath><clipPath
|
||||||
|
id="clipPath1905"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1907" /></clipPath><clipPath
|
||||||
|
id="clipPath1917"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1919" /></clipPath><clipPath
|
||||||
|
id="clipPath1929"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1931" /></clipPath><clipPath
|
||||||
|
id="clipPath1941"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1943" /></clipPath><clipPath
|
||||||
|
id="clipPath1953"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1955" /></clipPath><clipPath
|
||||||
|
id="clipPath1965"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1967" /></clipPath><clipPath
|
||||||
|
id="clipPath1977"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1979" /></clipPath><clipPath
|
||||||
|
id="clipPath1989"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path1991" /></clipPath><clipPath
|
||||||
|
id="clipPath2001"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2003" /></clipPath><clipPath
|
||||||
|
id="clipPath2013"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2015" /></clipPath><clipPath
|
||||||
|
id="clipPath2025"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2027" /></clipPath><clipPath
|
||||||
|
id="clipPath2037"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2039" /></clipPath><clipPath
|
||||||
|
id="clipPath2049"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2051" /></clipPath><clipPath
|
||||||
|
id="clipPath2061"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2063" /></clipPath><clipPath
|
||||||
|
id="clipPath2073"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2075" /></clipPath><clipPath
|
||||||
|
id="clipPath2085"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2087" /></clipPath><clipPath
|
||||||
|
id="clipPath2097"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2099" /></clipPath><clipPath
|
||||||
|
id="clipPath2109"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2111" /></clipPath><clipPath
|
||||||
|
id="clipPath2121"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2123" /></clipPath><clipPath
|
||||||
|
id="clipPath2133"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2135" /></clipPath><clipPath
|
||||||
|
id="clipPath2145"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2147" /></clipPath><clipPath
|
||||||
|
id="clipPath2157"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2159" /></clipPath><clipPath
|
||||||
|
id="clipPath2169"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2171" /></clipPath><clipPath
|
||||||
|
id="clipPath2181"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2183" /></clipPath><clipPath
|
||||||
|
id="clipPath2193"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2195" /></clipPath><clipPath
|
||||||
|
id="clipPath2205"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2207" /></clipPath><clipPath
|
||||||
|
id="clipPath2217"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2219" /></clipPath><clipPath
|
||||||
|
id="clipPath2229"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2231" /></clipPath><clipPath
|
||||||
|
id="clipPath2241"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2243" /></clipPath><clipPath
|
||||||
|
id="clipPath2253"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2255" /></clipPath><clipPath
|
||||||
|
id="clipPath2265"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2267" /></clipPath><clipPath
|
||||||
|
id="clipPath2277"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2279" /></clipPath><clipPath
|
||||||
|
id="clipPath2289"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2291" /></clipPath><clipPath
|
||||||
|
id="clipPath2301"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2303" /></clipPath><clipPath
|
||||||
|
id="clipPath2313"><path
|
||||||
|
d="M 0,0 L 1323.78,0 L 1323.78,666.142 L 0,666.142 L 0,0 z"
|
||||||
|
id="path2315" /></clipPath></defs><g
|
||||||
|
transform="matrix(1.25,0,0,-1.25,-1258.5674,129.11175)"
|
||||||
|
id="g11"><g
|
||||||
|
id="g79"><g
|
||||||
|
id="g297"><g
|
||||||
|
clip-path="url(#clipPath301)"
|
||||||
|
id="g299"><g
|
||||||
|
transform="translate(708.2288,51.2175)"
|
||||||
|
id="g305"><path
|
||||||
|
d="M 0,0 C -0.578,0 -1.054,0.458 -1.054,1.02 C -1.054,1.597 -0.578,2.055 0,2.055 C 0.577,2.055 1.068,1.613 1.068,1.02 C 1.068,0.442 0.577,0 0,0 M 0.797,-9.936 L -0.799,-9.936 L -0.799,-1.886 L 0.797,-1.886 L 0.797,-9.936 z"
|
||||||
|
id="path307"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
id="g309"><g
|
||||||
|
clip-path="url(#clipPath313)"
|
||||||
|
id="g311"><g
|
||||||
|
transform="translate(715.5793,48.0258)"
|
||||||
|
id="g317"><path
|
||||||
|
d="M 0,0 L -2.139,0 L -2.139,-4.195 C -2.139,-5.166 -1.799,-5.554 -1.002,-5.554 C -0.713,-5.554 -0.341,-5.486 -0.067,-5.351 L -0.016,-6.658 C -0.392,-6.793 -0.882,-6.878 -1.374,-6.878 C -2.885,-6.878 -3.719,-6.029 -3.719,-4.365 L -3.719,0 L -5.262,0 L -5.262,1.306 L -3.719,1.306 L -3.719,3.617 L -2.139,3.617 L -2.139,1.306 L 0,1.306 L 0,0 z"
|
||||||
|
id="path319"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
id="g321"><g
|
||||||
|
clip-path="url(#clipPath325)"
|
||||||
|
id="g323"><g
|
||||||
|
transform="translate(718.7208,48.1097)"
|
||||||
|
id="g329"><path
|
||||||
|
d="M 0,0 C 0.374,0.797 1.393,1.443 2.546,1.443 C 4.619,1.443 5.552,-0.033 5.552,-1.833 L 5.552,-6.826 L 3.958,-6.826 L 3.958,-2.378 C 3.958,-1.07 3.617,0.083 2.139,0.083 C 0.885,0.083 -0.033,-0.968 -0.033,-2.564 L -0.033,-6.826 L -1.632,-6.826 L -1.632,6.013 L -0.033,6.013 L -0.033,0 L 0,0 z"
|
||||||
|
id="path331"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
id="g333"><g
|
||||||
|
clip-path="url(#clipPath337)"
|
||||||
|
id="g335"><g
|
||||||
|
transform="translate(733.9358,43.0148)"
|
||||||
|
id="g341"><path
|
||||||
|
d="M 0,0 L 0.033,0 L 2.261,6.317 L 3.957,6.317 L 0.052,-3.636 C -0.476,-4.977 -1.153,-5.808 -2.732,-5.808 C -3.072,-5.808 -3.43,-5.773 -3.786,-5.687 L -3.636,-4.278 C -3.38,-4.363 -3.091,-4.416 -2.835,-4.416 C -2,-4.416 -1.679,-3.94 -1.339,-3.074 L -0.831,-1.731 L -4.278,6.317 L -2.495,6.317 L 0,0 z"
|
||||||
|
id="path343"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
id="g345"><g
|
||||||
|
clip-path="url(#clipPath349)"
|
||||||
|
id="g347"><g
|
||||||
|
transform="translate(743.0728,41.0614)"
|
||||||
|
id="g353"><path
|
||||||
|
d="M 0,0 C -2.462,0 -4.331,1.783 -4.331,4.264 C -4.331,6.741 -2.462,8.491 0,8.491 C 2.461,8.491 4.348,6.741 4.348,4.264 C 4.348,1.783 2.461,0 0,0 M 0,7.167 C -1.715,7.167 -2.685,5.789 -2.685,4.264 C -2.685,2.734 -1.715,1.344 0,1.344 C 1.733,1.344 2.685,2.734 2.685,4.264 C 2.685,5.789 1.733,7.167 0,7.167"
|
||||||
|
id="path355"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
id="g357"><g
|
||||||
|
clip-path="url(#clipPath361)"
|
||||||
|
id="g359"><g
|
||||||
|
transform="translate(755.131,41.282)"
|
||||||
|
id="g365"><path
|
||||||
|
d="M 0,0 C -0.036,0.407 -0.069,0.987 -0.069,1.325 L -0.102,1.325 C -0.512,0.476 -1.562,-0.221 -2.735,-0.221 C -4.806,-0.221 -5.742,1.258 -5.742,3.057 L -5.742,8.05 L -4.144,8.05 L -4.144,3.6 C -4.144,2.276 -3.788,1.124 -2.31,1.124 C -1.036,1.124 -0.153,2.174 -0.153,3.77 L -0.153,8.05 L 1.445,8.05 L 1.445,1.73 C 1.445,1.309 1.46,0.528 1.511,0 L 0,0 z"
|
||||||
|
id="path367"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
id="g369"><g
|
||||||
|
clip-path="url(#clipPath373)"
|
||||||
|
id="g371"><g
|
||||||
|
transform="translate(768.2584,46.4964)"
|
||||||
|
id="g377"><path
|
||||||
|
d="M 0,0 C 0,1.187 -0.801,1.767 -1.986,1.767 C -2.889,1.767 -3.738,1.375 -4.28,0.83 L -5.128,1.852 C -4.349,2.599 -3.142,3.056 -1.837,3.056 C 0.475,3.056 1.528,1.681 1.528,-0.102 L 1.528,-3.617 C 1.528,-4.162 1.561,-4.823 1.646,-5.214 L 0.204,-5.214 C 0.117,-4.856 0.066,-4.434 0.066,-4.06 L 0.015,-4.06 C -0.563,-4.925 -1.479,-5.417 -2.686,-5.417 C -3.958,-5.417 -5.604,-4.788 -5.604,-2.939 C -5.604,-0.51 -2.717,-0.22 0,-0.22 L 0,0 z M -0.391,-1.376 C -1.971,-1.376 -3.976,-1.546 -3.976,-2.903 C -3.976,-3.857 -3.127,-4.162 -2.327,-4.162 C -0.834,-4.162 0,-3.16 0,-1.802 L 0,-1.376 L -0.391,-1.376 z"
|
||||||
|
id="path379"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
id="g381"><g
|
||||||
|
clip-path="url(#clipPath385)"
|
||||||
|
id="g383"><path
|
||||||
|
d="M 773.913,41.284 L 772.315,41.284 L 772.315,54.123 L 773.913,54.123 L 773.913,41.284 z"
|
||||||
|
id="path389"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
|
||||||
|
d="M 778.158,41.284 L 776.56,41.284 L 776.56,54.123 L 778.158,54.123 L 778.158,41.284 z"
|
||||||
|
id="path391"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /><g
|
||||||
|
transform="translate(788.7883,48.0258)"
|
||||||
|
id="g393"><path
|
||||||
|
d="M 0,0 L -2.14,0 L -2.14,-4.195 C -2.14,-5.166 -1.8,-5.554 -1.003,-5.554 C -0.714,-5.554 -0.34,-5.486 -0.067,-5.351 L -0.018,-6.658 C -0.388,-6.793 -0.885,-6.878 -1.375,-6.878 C -2.886,-6.878 -3.719,-6.029 -3.719,-4.365 L -3.719,0 L -5.263,0 L -5.263,1.306 L -3.719,1.306 L -3.719,3.617 L -2.14,3.617 L -2.14,1.306 L 0,1.306 L 0,0 z"
|
||||||
|
id="path395"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||||
|
transform="translate(791.9298,48.1097)"
|
||||||
|
id="g397"><path
|
||||||
|
d="M 0,0 C 0.373,0.797 1.39,1.443 2.546,1.443 C 4.619,1.443 5.553,-0.033 5.553,-1.833 L 5.553,-6.826 L 3.958,-6.826 L 3.958,-2.378 C 3.958,-1.07 3.617,0.083 2.139,0.083 C 0.881,0.083 -0.036,-0.968 -0.036,-2.564 L -0.036,-6.826 L -1.632,-6.826 L -1.632,6.013 L -0.036,6.013 L -0.036,0 L 0,0 z"
|
||||||
|
id="path399"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||||
|
transform="translate(806.9933,44.83)"
|
||||||
|
id="g401"><path
|
||||||
|
d="M 0,0 L -6.388,0 C -6.334,-1.373 -5.183,-2.443 -3.77,-2.443 C -2.684,-2.443 -1.888,-1.901 -1.427,-1.221 L -0.307,-2.122 C -1.174,-3.243 -2.344,-3.769 -3.77,-3.769 C -6.217,-3.769 -8.016,-2.069 -8.016,0.46 C -8.016,2.973 -6.217,4.724 -3.854,4.724 C -1.478,4.724 0.015,3.11 0.015,0.545 C 0.015,0.375 0.015,0.189 0,0 M -1.598,1.19 C -1.632,2.464 -2.377,3.468 -3.854,3.468 C -5.234,3.468 -6.283,2.447 -6.388,1.19 L -1.598,1.19 z"
|
||||||
|
id="path403"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||||
|
transform="translate(813.7344,49.3321)"
|
||||||
|
id="g405"><path
|
||||||
|
d="M 0,0 L 1.814,-6.147 L 1.85,-6.147 L 3.771,0 L 5.45,0 L 7.404,-6.147 L 7.44,-6.147 L 9.253,0 L 10.936,0 L 8.27,-8.05 L 6.605,-8.05 L 4.619,-2.006 L 4.583,-2.006 L 2.615,-8.05 L 0.951,-8.05 L -1.734,0 L 0,0 z"
|
||||||
|
id="path407"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||||
|
transform="translate(831.1046,46.4964)"
|
||||||
|
id="g409"><path
|
||||||
|
d="M 0,0 C 0,1.187 -0.794,1.767 -1.986,1.767 C -2.886,1.767 -3.734,1.375 -4.276,0.83 L -5.124,1.852 C -4.345,2.599 -3.142,3.056 -1.833,3.056 C 0.479,3.056 1.529,1.681 1.529,-0.102 L 1.529,-3.617 C 1.529,-4.162 1.564,-4.823 1.649,-5.214 L 0.204,-5.214 C 0.121,-4.856 0.072,-4.434 0.072,-4.06 L 0.017,-4.06 C -0.559,-4.925 -1.475,-5.417 -2.681,-5.417 C -3.954,-5.417 -5.603,-4.788 -5.603,-2.939 C -5.603,-0.51 -2.714,-0.22 0,-0.22 L 0,0 z M -0.388,-1.376 C -1.968,-1.376 -3.972,-1.546 -3.972,-2.903 C -3.972,-3.857 -3.123,-4.162 -2.326,-4.162 C -0.831,-4.162 0,-3.16 0,-1.802 L 0,-1.376 L -0.388,-1.376 z"
|
||||||
|
id="path411"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||||
|
transform="translate(838.1888,43.0148)"
|
||||||
|
id="g413"><path
|
||||||
|
d="M 0,0 L 0.032,0 L 2.26,6.317 L 3.956,6.317 L 0.051,-3.636 C -0.475,-4.977 -1.155,-5.808 -2.736,-5.808 C -3.076,-5.808 -3.432,-5.773 -3.789,-5.687 L -3.635,-4.278 C -3.381,-4.363 -3.091,-4.416 -2.837,-4.416 C -2.004,-4.416 -1.681,-3.94 -1.345,-3.074 L -0.834,-1.731 L -4.279,6.317 L -2.497,6.317 L 0,0 z"
|
||||||
|
id="path415"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||||
|
transform="translate(694.152,43.7708)"
|
||||||
|
id="g417"><path
|
||||||
|
d="M 0,0 L 0.035,0 L 2.819,9.459 L 4.645,9.459 L 7.414,0 L 7.447,0 L 10.062,9.459 L 11.822,9.459 L 8.336,-2.417 L 6.609,-2.417 L 3.723,7.126 L 3.691,7.126 L 0.805,-2.417 L -0.922,-2.417 L -4.411,9.459 L -2.617,9.459 L 0,0 z"
|
||||||
|
id="path419"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,1056.6001,101.86204)"
|
||||||
|
id="g421"><path
|
||||||
|
d="M 0,0 L 0,-11.324 L -8.867,0 L -11.368,0 L -11.368,-16.28 L -8.64,-16.28 L -8.64,-4.411 L 0.591,-16.28 L 2.683,-16.28 L 2.683,0 L 0,0 z"
|
||||||
|
id="path423"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||||
|
id="g425"><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g427"><g
|
||||||
|
clip-path="url(#clipPath431)"
|
||||||
|
id="g429"><g
|
||||||
|
transform="translate(1178.0209,88.8605)"
|
||||||
|
id="g435"><path
|
||||||
|
d="M 0,0 C -3.184,0 -5.502,2.592 -5.502,5.775 C -5.548,8.913 -3.138,11.505 0,11.505 C 3.183,11.505 5.502,8.913 5.502,5.775 C 5.502,2.592 3.183,0 0,0 M 0,14.097 C -4.639,14.097 -8.322,10.413 -8.322,5.775 C -8.322,1.092 -4.729,-2.592 0,-2.592 C 4.683,-2.592 8.321,1.092 8.321,5.775 C 8.321,10.413 4.638,14.097 0,14.097"
|
||||||
|
id="path437"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g439"><g
|
||||||
|
clip-path="url(#clipPath443)"
|
||||||
|
id="g441"><g
|
||||||
|
transform="translate(1113.8667,88.8597)"
|
||||||
|
id="g447"><path
|
||||||
|
d="M 0,0 C -3.183,0 -5.502,2.593 -5.502,5.776 C -5.548,8.914 -3.137,11.506 0,11.506 C 3.183,11.506 5.503,8.914 5.503,5.776 C 5.503,2.593 3.183,0 0,0 M 0,14.098 C -4.639,14.098 -8.322,10.414 -8.322,5.776 C -8.322,1.092 -4.729,-2.591 0,-2.591 C 4.684,-2.591 8.322,1.092 8.322,5.776 C 8.322,10.414 4.638,14.098 0,14.098"
|
||||||
|
id="path449"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g451"><g
|
||||||
|
clip-path="url(#clipPath455)"
|
||||||
|
id="g453"><g
|
||||||
|
transform="translate(1095.1572,102.5486)"
|
||||||
|
id="g459"><path
|
||||||
|
d="M 0,0 L -6.776,-7.639 L -6.776,0 L -9.503,0 L -9.503,-16.28 L -6.776,-16.28 L -6.776,-8.504 L 0.137,-16.28 L 3.82,-16.28 L -3.682,-7.959 L 3.457,0 L 0,0 z"
|
||||||
|
id="path461"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g463"><g
|
||||||
|
clip-path="url(#clipPath467)"
|
||||||
|
id="g465"><g
|
||||||
|
transform="translate(1145.6705,101.7752)"
|
||||||
|
id="g471"><path
|
||||||
|
d="M 0,0 C -1.137,-0.818 -1.728,-2 -1.728,-3.41 C -1.728,-6.138 0.409,-7.366 2.592,-8.049 C 4.547,-8.685 6.139,-9.186 6.139,-10.777 C 6.139,-12.323 4.639,-13.005 3.228,-13.005 C 1.41,-13.005 -0.319,-12.278 -1.365,-11.596 L -1.456,-11.551 L -1.456,-14.324 L -1.41,-14.324 C -0.455,-14.869 1.318,-15.507 3.273,-15.507 C 6.139,-15.507 8.821,-13.642 8.821,-10.687 C 8.821,-7.958 6.957,-6.639 4.365,-5.82 C 1.681,-5.002 0.863,-4.411 0.863,-3.364 C 0.863,-1.864 2.409,-1.409 3.819,-1.409 C 4.91,-1.409 6.366,-1.682 7.684,-2.319 L 7.731,-2.319 L 7.731,0.273 C 6.502,0.819 5.184,1.092 3.638,1.092 C 2.273,1.092 0.954,0.683 0,0"
|
||||||
|
id="path473"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g475"><g
|
||||||
|
clip-path="url(#clipPath479)"
|
||||||
|
id="g477"><g
|
||||||
|
transform="translate(1253.6062,88.8605)"
|
||||||
|
id="g483"><path
|
||||||
|
d="M 0,0 L -2.411,0 L -2.411,11.05 L 0,11.05 C 3.228,11.05 5.502,8.686 5.502,5.548 C 5.502,2.41 3.228,0 0,0 M 0,13.688 L -5.139,13.688 L -5.139,-2.592 L 0,-2.592 C 4.639,-2.592 8.366,0.864 8.366,5.548 C 8.366,10.231 4.593,13.688 0,13.688"
|
||||||
|
id="path485"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g487"><g
|
||||||
|
clip-path="url(#clipPath491)"
|
||||||
|
id="g489"><g
|
||||||
|
transform="translate(1202.0627,102.5478)"
|
||||||
|
id="g495"><path
|
||||||
|
d="M 0,0 L 0,-16.279 L 9.023,-16.279 L 10.278,-13.597 L 2.774,-13.597 L 2.774,0 L 0,0 z"
|
||||||
|
id="path497"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g499"><g
|
||||||
|
clip-path="url(#clipPath503)"
|
||||||
|
id="g501"><g
|
||||||
|
transform="translate(1044.2117,88.7691)"
|
||||||
|
id="g507"><path
|
||||||
|
d="M 0,0 L -2.819,0 L -2.819,4.593 L 0.5,4.593 C 2.046,4.593 3.092,3.684 3.092,2.319 C 3.092,0.865 2,0 0,0 M -2.819,11.278 L 0.091,11.278 C 1.41,11.278 2.592,10.596 2.592,9.232 C 2.592,7.913 1.592,7.05 0.227,7.05 L -2.819,7.05 L -2.819,11.278 z M 3.502,6.185 C 4.456,6.686 5.32,7.913 5.32,9.414 C 5.32,12.279 3.092,13.779 0.318,13.779 L -5.548,13.779 L -5.548,-2.5 L 0.136,-2.5 C 3.32,-2.5 5.911,-1.091 5.911,2.229 C 5.911,3.911 5.047,5.275 3.502,6.185"
|
||||||
|
id="path509"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g511"><g
|
||||||
|
clip-path="url(#clipPath515)"
|
||||||
|
id="g513"><g
|
||||||
|
transform="translate(1242.7509,102.5484)"
|
||||||
|
id="g519"><path
|
||||||
|
d="M 0,0 L 0,-11.324 L -8.867,0 L -11.368,0 L -11.368,-16.28 L -8.685,-16.28 L -8.685,-4.411 L 0.592,-16.28 L 2.683,-16.28 L 2.683,0 L 0,0 z"
|
||||||
|
id="path521"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g523"><g
|
||||||
|
clip-path="url(#clipPath527)"
|
||||||
|
id="g525"><g
|
||||||
|
transform="translate(1056.5291,93.2268)"
|
||||||
|
id="g531"><path
|
||||||
|
d="M 0,0 L 2.41,6.093 L 4.729,0 L 0,0 z M 3.728,9.367 L 1,9.367 L -5.593,-6.958 L -2.683,-6.958 L -0.956,-2.502 L 5.685,-2.502 L 7.458,-6.958 L 10.413,-6.958 L 3.728,9.367 z"
|
||||||
|
id="path533"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g535"><g
|
||||||
|
clip-path="url(#clipPath539)"
|
||||||
|
id="g537"><g
|
||||||
|
transform="translate(1218.8624,93.2268)"
|
||||||
|
id="g543"><path
|
||||||
|
d="M 0,0 L 2.41,6.093 L 4.729,0 L 0,0 z M 3.729,9.367 L 1,9.367 L -5.592,-6.958 L -2.682,-6.958 L -0.955,-2.502 L 5.685,-2.502 L 7.458,-6.958 L 10.413,-6.958 L 3.729,9.367 z"
|
||||||
|
id="path545"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g547"><g
|
||||||
|
clip-path="url(#clipPath551)"
|
||||||
|
id="g549"><g
|
||||||
|
transform="translate(1164.6093,100.4336)"
|
||||||
|
id="g555"><path
|
||||||
|
d="M 0,0 C 1.182,0 2.547,-0.318 3.639,-0.682 L 3.639,2.047 C 2.819,2.319 1.456,2.638 0,2.638 C -4.983,2.638 -8.72,-0.646 -8.813,-5.457 C -8.813,-5.462 -8.813,-5.467 -8.814,-5.472 C -8.815,-5.528 -8.821,-5.582 -8.821,-5.638 C -8.821,-5.681 -8.82,-5.721 -8.819,-5.764 C -8.82,-5.806 -8.821,-5.845 -8.821,-5.889 C -8.821,-5.946 -8.815,-5.999 -8.814,-6.055 C -8.813,-6.06 -8.813,-6.065 -8.813,-6.07 C -8.72,-10.881 -4.983,-14.165 0,-14.165 C 1.456,-14.165 2.819,-13.847 3.639,-13.573 L 3.639,-10.845 C 2.547,-11.209 1.182,-11.527 0,-11.527 C -3.593,-11.527 -6.047,-9.026 -6.003,-5.889 C -6.003,-5.847 -6.005,-5.805 -6.006,-5.764 C -6.005,-5.721 -6.003,-5.68 -6.003,-5.638 C -6.047,-2.501 -3.593,0 0,0"
|
||||||
|
id="path557"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g559"><g
|
||||||
|
clip-path="url(#clipPath563)"
|
||||||
|
id="g561"><g
|
||||||
|
transform="translate(1134.9301,99.8654)"
|
||||||
|
id="g567"><path
|
||||||
|
d="M 0,0 L 0,2.682 L -10.277,2.682 L -10.277,-13.597 L -7.503,-13.597 L -7.503,-6.54 L -0.653,-6.54 L -0.653,-3.857 L -7.503,-3.857 L -7.503,0 L 0,0 z"
|
||||||
|
id="path569"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g571"><g
|
||||||
|
clip-path="url(#clipPath575)"
|
||||||
|
id="g573"><g
|
||||||
|
transform="translate(1199.6188,102.5461)"
|
||||||
|
id="g579"><path
|
||||||
|
d="M 0,0 L -12.974,0 L -12.974,-2.682 L -7.923,-2.682 L -7.923,-16.279 L -5.051,-16.279 L -5.051,-2.682 L 0,-2.682 L 0,0 z"
|
||||||
|
id="path581"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g583"><g
|
||||||
|
clip-path="url(#clipPath587)"
|
||||||
|
id="g585"><g
|
||||||
|
transform="translate(1022.0269,101.0614)"
|
||||||
|
id="g591"><path
|
||||||
|
d="M 0,0 C 0,1.23 -0.992,2.228 -2.227,2.228 C -3.453,2.226 -4.452,1.23 -4.454,0 C -4.452,-1.228 -3.453,-2.226 -2.227,-2.227 C -0.992,-2.226 0,-1.228 0,0"
|
||||||
|
id="path593"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g595"><g
|
||||||
|
clip-path="url(#clipPath599)"
|
||||||
|
id="g597"><g
|
||||||
|
transform="translate(1022.0269,87.8022)"
|
||||||
|
id="g603"><path
|
||||||
|
d="M 0,0 C 0,1.229 -0.992,2.227 -2.227,2.227 C -3.453,2.225 -4.452,1.229 -4.454,0 C -4.452,-1.229 -3.453,-2.226 -2.227,-2.228 C -0.992,-2.226 0,-1.229 0,0"
|
||||||
|
id="path605"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g607"><g
|
||||||
|
clip-path="url(#clipPath611)"
|
||||||
|
id="g609"><g
|
||||||
|
transform="translate(1011.3079,94.4106)"
|
||||||
|
id="g615"><path
|
||||||
|
d="M 0,0 C 0,1.23 -0.992,2.228 -2.227,2.228 C -3.453,2.226 -4.451,1.23 -4.454,0 C -4.451,-1.229 -3.453,-2.226 -2.227,-2.228 C -0.992,-2.226 0,-1.229 0,0"
|
||||||
|
id="path617"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g619"><g
|
||||||
|
clip-path="url(#clipPath623)"
|
||||||
|
id="g621"><g
|
||||||
|
transform="translate(1032.9076,94.411)"
|
||||||
|
id="g627"><path
|
||||||
|
d="M 0,0 C 0,1.23 -0.992,2.228 -2.227,2.228 C -3.453,2.226 -4.452,1.23 -4.454,0 C -4.452,-1.229 -3.453,-2.225 -2.227,-2.228 C -0.992,-2.225 0,-1.229 0,0"
|
||||||
|
id="path629"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
transform="matrix(0.6605755,0,0,0.6605755,343.15792,34.120936)"
|
||||||
|
id="g631"><g
|
||||||
|
clip-path="url(#clipPath635)"
|
||||||
|
id="g633"><g
|
||||||
|
transform="translate(1029.8667,98.3729)"
|
||||||
|
id="g639"><path
|
||||||
|
d="M 0,0 L 0,4.173 L -10.017,-1.886 L -10.071,-1.918 L -20.133,4.177 L -20.133,0.005 L -13.519,-3.964 L -20.133,-7.926 L -20.133,-12.101 L -10.07,-6.013 L 0,-12.101 L 0,-7.926 L -6.619,-3.964 L 0,0 z"
|
||||||
|
id="path641"
|
||||||
|
style="fill:#1e357a;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><g
|
||||||
|
id="g767" /></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="#00AEEF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Barclays</title><path d="M21.043 3.629a3.235 3.235 0 0 0-1.048-.54 3.076 3.076 0 0 0-.937-.144h-.046c-.413.006-1.184.105-1.701.71a1.138 1.138 0 0 0-.226 1.023.9.9 0 0 0 .555.63s.088.032.228.058c-.04.078-.136.214-.136.214-.179.265-.576.612-1.668.612h-.063c-.578-.038-1.056-.189-1.616-.915-.347-.45-.523-1.207-.549-2.452-.022-.624-.107-1.165-.256-1.6-.1-.29-.333-.596-.557-.742a2.55 2.55 0 0 0-.694-.336c-.373-.12-.848-.14-1.204-.146-.462-.01-.717.096-.878.292-.027.033-.032.05-.068.046-.084-.006-.272-.006-.328-.006-.264 0-.498.043-.721.09-.47.1-.761.295-1.019.503-.12.095-.347.365-.399.653a.76.76 0 0 0 .097.578c.14-.148.374-.264.816-.266.493-.002 1.169.224 1.406.608.336.547.27.99.199 1.517-.183 1.347-.68 2.048-1.783 2.203-.191.026-.38.04-.56.04-.776 0-1.34-.248-1.63-.716a.71.71 0 0 1-.088-.168s.087-.021.163-.056c.294-.14.514-.344.594-.661.09-.353.004-.728-.23-1.007-.415-.47-.991-.708-1.713-.708-.4 0-.755.076-.982.14-.908.256-1.633.947-2.214 2.112-.412.824-.7 1.912-.81 3.067-.11 1.13-.056 2.085.019 2.949.124 1.437.363 2.298.708 3.22a15.68 15.68 0 0 0 1.609 3.19c.09-.094.15-.161.308-.318.188-.19.724-.893.876-1.11.19-.27.51-.779.664-1.147l.15.119c.16.127.252.348.249.592-.003.215-.053.464-.184.922a8.703 8.703 0 0 1-.784 1.818c-.189.341-.27.508-.199.584.015.015.038.03.06.026.116 0 .34-.117.585-.304.222-.17.813-.672 1.527-1.675a15.449 15.449 0 0 0 1.452-2.521c.12.046.255.101.317.226a.92.92 0 0 1 .08.563c-.065.539-.379 1.353-.63 1.94-.425.998-1.208 2.115-1.788 2.877-.022.03-.163.197-.186.227.9.792 1.944 1.555 3.007 2.136.725.408 2.203 1.162 3.183 1.424.98-.262 2.458-1.016 3.184-1.424a17.063 17.063 0 0 0 3.003-2.134c-.05-.076-.13-.158-.183-.23-.582-.763-1.365-1.881-1.79-2.875-.25-.59-.563-1.405-.628-1.94-.028-.221-.002-.417.08-.565.033-.098.274-.218.317-.226.405.884.887 1.73 1.452 2.522.715 1.003 1.306 1.506 1.527 1.674.248.191.467.304.586.304a.07.07 0 0 0 .044-.012c.094-.069.017-.234-.183-.594a9.003 9.003 0 0 1-.786-1.822c-.13-.456-.18-.706-.182-.92-.004-.246.088-.466.248-.594l.15-.118c.155.373.5.919.665 1.147.15.216.685.919.876 1.11.156.158.22.222.308.32a15.672 15.672 0 0 0 1.609-3.19c.343-.923.583-1.784.707-3.222.075-.86.128-1.81.02-2.948-.101-1.116-.404-2.264-.81-3.068-.249-.49-.605-1.112-1.171-1.566z"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="#117ACA" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Chase</title><path d="M0 15.415c0 .468.38.85.848.85h5.937V.575L0 7.72v7.695m15.416 8.582c.467 0 .846-.38.846-.849v-5.937H.573l7.146 6.785h7.697M24 8.587a.844.844 0 0 0-.847-.846h-5.938V23.43l6.782-7.148L24 8.586M8.585.003a.847.847 0 0 0-.847.847v5.94h15.688L16.282.003H8.585Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 377 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="#DB0011" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>HSBC</title><path d="m24 12.007-5.996 5.997V5.996L24 12.007zm-5.996-6.01H6.01l5.996 6.01 5.997-6.01zM0 12.006l6.01 5.997V5.996L0 12.007zm6.01 5.997h11.994l-5.997-5.997-5.996 5.997z"/></svg>
|
||||||
|
After Width: | Height: | Size: 282 B |
@@ -0,0 +1,103 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="132.2917mm"
|
||||||
|
height="26.434887mm"
|
||||||
|
viewBox="0 0 132.2917 26.434886"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||||
|
sodipodi:docname="Lloyds wordmark.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="1.9729351"
|
||||||
|
inkscape:cx="211.36023"
|
||||||
|
inkscape:cy="108.72127"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1001"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(6.8759843,19.415936)">
|
||||||
|
<path
|
||||||
|
d="m 83.744561,121.44157 v -12.39095 c 0,-0.69286 -0.162434,-1.4841 -0.40081,-2.47042 h 3.985747 c -0.238377,0.98632 -0.400811,1.77605 -0.400811,2.46891 v 12.03335 h 4.365709 c 1.461739,0 2.38259,-0.0656 3.357027,-0.33525 l -0.98641,3.16328 H 83.343751 c 0.238376,-0.98633 0.40081,-1.77606 0.40081,-2.46892 z"
|
||||||
|
fill="#000000"
|
||||||
|
id="path3"
|
||||||
|
style="stroke-width:0.837289" />
|
||||||
|
<path
|
||||||
|
d="m 96.621145,121.44149 v -12.39096 c 0,-0.69285 -0.162434,-1.48401 -0.40081,-2.46891 h 3.985835 c -0.238464,0.98641 -0.400814,1.77606 -0.400814,2.46891 v 12.03335 h 4.365704 c 1.46174,0 2.38251,-0.0656 3.35703,-0.33525 l -0.9849,3.16336 H 96.220335 c 0.238376,-0.98641 0.40081,-1.77614 0.40081,-2.46899 z"
|
||||||
|
fill="#000000"
|
||||||
|
id="path4"
|
||||||
|
style="stroke-width:0.837289" />
|
||||||
|
<path
|
||||||
|
d="m 107.96022,115.24605 c 0,-4.9498 3.62814,-8.88196 8.84914,-8.88196 5.221,0 8.84897,3.93216 8.84897,8.88196 0,4.94989 -3.62797,8.88197 -8.84897,8.88197 -5.221,0 -8.84914,-3.93208 -8.84914,-8.88197 z m 14.48141,0 c 0,-3.57455 -2.12328,-6.04347 -5.63227,-6.04347 -3.509,0 -5.6322,2.46892 -5.6322,6.04347 0,3.57456 2.1232,6.04356 5.6322,6.04356 3.50899,0 5.63227,-2.469 5.63227,-6.04356 z"
|
||||||
|
fill="#000000"
|
||||||
|
id="path5"
|
||||||
|
style="stroke-width:0.837289" />
|
||||||
|
<path
|
||||||
|
d="m 132.00498,117.49895 c -2.65421,-2.70738 -4.96094,-6.3907 -6.13063,-10.91733 h 3.66146 c 0.0536,0.77926 0.19509,1.49448 0.48731,2.32883 0.83394,2.38259 2.10076,4.37618 3.55178,6.06591 1.49456,-1.61371 2.87022,-3.68332 3.70416,-6.06591 0.29222,-0.83435 0.43372,-1.54806 0.48731,-2.32883 h 3.55261 c -1.15964,4.52814 -3.47642,8.20995 -6.13063,10.91733 v 6.41154 h -3.18337 z"
|
||||||
|
fill="#000000"
|
||||||
|
id="path6"
|
||||||
|
style="stroke-width:0.837289" />
|
||||||
|
<path
|
||||||
|
d="m 143.91542,121.44149 v -12.39096 c 0,-0.69285 -0.16244,-1.48401 -0.40023,-2.46891 h 5.84847 c 5.55625,0 9.06449,3.5522 9.06449,8.66443 0,5.11224 -3.50824,8.66444 -9.06449,8.66444 h -5.84847 c 0.23779,-0.98641 0.40023,-1.77614 0.40023,-2.469 z m 11.29754,-6.2059 c 0,-3.75784 -2.13341,-5.82745 -6.06532,-5.82745 h -2.04801 v 11.67574 h 2.04801 c 3.93191,0 6.06532,-2.0681 6.06532,-5.84829 z"
|
||||||
|
fill="#000000"
|
||||||
|
id="path7"
|
||||||
|
style="stroke-width:0.837289" />
|
||||||
|
<path
|
||||||
|
d="m 160.61598,122.93588 -0.0536,-3.46579 c 2.19872,1.41854 3.67151,2.06819 5.38293,2.06819 1.94921,0 3.19509,-0.76888 3.19509,-2.1561 0,-0.92077 -0.53084,-1.63598 -2.27575,-2.3393 l -2.78315,-1.11603 c -2.56712,-1.02961 -3.71421,-2.44806 -3.71421,-4.66822 0,-3.00084 2.33938,-4.89613 5.94643,-4.89613 1.88473,0 3.66062,0.59598 4.69049,1.0832 v 3.33618 c -1.94921,-1.28892 -3.3793,-1.81935 -4.84121,-1.81935 -1.81943,0 -2.71867,0.71522 -2.71867,1.80888 0,1.09367 0.57354,1.64645 1.93832,2.20969 l 3.49819,1.41854 c 2.03713,0.82247 3.36926,2.30648 3.36926,4.57134 0,2.96811 -2.31846,5.15544 -6.44462,5.15544 -2.16607,0 -4.03908,-0.68247 -5.18784,-1.19204 z"
|
||||||
|
fill="#000000"
|
||||||
|
id="path8"
|
||||||
|
style="stroke-width:0.837289" />
|
||||||
|
<path
|
||||||
|
d="M -6.2795836,3.0215555 V -15.416049 c 0,-1.030962 -0.2416999,-2.208316 -0.5964007,-3.675959 h 5.93074667 c -0.35470227,1.467643 -0.59640227,2.642754 -0.59640227,3.673716 V 2.487199 H 4.954486 c 2.1750512,0 3.5452671,-0.097552 4.9952184,-0.498849 L 8.4819374,6.6952721 H -6.8759843 c 0.3547008,-1.4676427 0.5964007,-2.6427536 0.5964007,-3.6737166 z"
|
||||||
|
fill="#000000"
|
||||||
|
id="path3-4"
|
||||||
|
style="stroke-width:1.24588" />
|
||||||
|
<path
|
||||||
|
d="M 12.880629,3.0214309 V -15.416173 c 0,-1.030963 -0.2417,-2.208192 -0.596401,-3.673718 H 18.2151 c -0.354826,1.467769 -0.596401,2.642755 -0.596401,3.673718 v 17.90549 h 6.496124 c 2.175053,0 3.545142,-0.097552 4.995219,-0.498849 L 27.644517,6.6975147 H 12.284228 c 0.354701,-1.4677673 0.596401,-2.6428783 0.596401,-3.6738412 z"
|
||||||
|
fill="#000000"
|
||||||
|
id="path4-1"
|
||||||
|
style="stroke-width:1.24588" />
|
||||||
|
<path
|
||||||
|
d="m 29.753039,-6.197309 c 0,-7.36525 5.398633,-13.216259 13.16742,-13.216259 7.76879,0 13.167173,5.851009 13.167173,13.216259 0,7.3653738 -5.398383,13.2162599 -13.167173,13.2162599 -7.768787,0 -13.16742,-5.8508861 -13.16742,-13.2162599 z m 21.548184,0 c 0,-5.318897 -3.159418,-8.992614 -8.380764,-8.992614 -5.221343,0 -8.380638,3.673717 -8.380638,8.992614 0,5.31889676 3.159295,8.9927379 8.380638,8.9927379 5.221346,0 8.380764,-3.67384114 8.380764,-8.9927379 z"
|
||||||
|
fill="#000000"
|
||||||
|
id="path5-1"
|
||||||
|
style="stroke-width:1.24588" />
|
||||||
|
<path
|
||||||
|
d="M 65.531377,-2.8450286 C 61.581948,-6.8735709 58.149559,-12.354307 56.409069,-19.089891 h 5.448218 c 0.07974,1.159538 0.290289,2.223767 0.7251,3.465282 1.240894,3.545267 3.125905,6.5116994 5.285009,9.0260031 2.22389,-2.401178 4.270865,-5.4807361 5.511759,-9.0260031 0.434811,-1.241515 0.645364,-2.3035 0.7251,-3.465282 h 5.286254 c -1.725539,6.737826 -5.17288,12.2163201 -9.122309,16.2448624 v 9.5403007 h -4.736823 z"
|
||||||
|
fill="#000000"
|
||||||
|
id="path6-3"
|
||||||
|
style="stroke-width:1.24588" />
|
||||||
|
<path
|
||||||
|
d="M 83.253973,3.0214309 V -15.416173 c 0,-1.030963 -0.2417,-2.208192 -0.595528,-3.673718 h 8.702448 c 8.267638,0 13.487857,5.285633 13.487857,12.892582 0,7.6069492 -5.220219,12.8925811 -13.487857,12.8925811 h -8.702448 c 0.353828,-1.4677673 0.595528,-2.6428782 0.595528,-3.6738412 z m 16.810617,-9.2343133 c 0,-5.5916186 -3.174497,-8.6711766 -9.025134,-8.6711766 H 87.992042 V 2.489317 h 3.047414 c 5.850637,0 9.025134,-3.07731535 9.025134,-8.7021994 z"
|
||||||
|
fill="#000000"
|
||||||
|
id="path7-8"
|
||||||
|
style="stroke-width:1.24588" />
|
||||||
|
<path
|
||||||
|
d="m 108.10423,5.2450716 -0.0797,-5.15705731 c 3.27167,2.11076431 5.46317,3.07744001 8.00974,3.07744001 2.9004,0 4.75427,-1.1440886 4.75427,-3.20825699 0,-1.37009061 -0.78989,-2.43431851 -3.3863,-3.48085491 l -4.14129,-1.6606291 c -3.81986,-1.5320545 -5.52671,-3.6426941 -5.52671,-6.9462613 0,-4.465222 3.48098,-7.285388 8.84822,-7.285388 2.80447,0 5.44697,0.886815 6.97941,1.611791 v 4.964195 c -2.90041,-1.917902 -5.02837,-2.707164 -7.20367,-2.707164 -2.70729,0 -4.04536,1.064227 -4.04536,2.691591 0,1.627364 0.85343,2.449892 2.8842,3.287993 l 5.20528,2.1107643 c 3.03122,1.2238247 5.0134,3.4320166 5.0134,6.8021129 0,4.4165082 -3.44983,7.6712364 -9.58951,7.6712364 -3.22308,0 -6.01011,-1.015514 -7.71945,-1.7737546 z"
|
||||||
|
fill="#000000"
|
||||||
|
id="path8-7"
|
||||||
|
style="stroke-width:1.24588" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="#EB001B" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>MasterCard</title><path d="M11.343 18.031c.058.049.12.098.181.146-1.177.783-2.59 1.238-4.107 1.238C3.32 19.416 0 16.096 0 12c0-4.095 3.32-7.416 7.416-7.416 1.518 0 2.931.456 4.105 1.238-.06.051-.12.098-.165.15C9.6 7.489 8.595 9.688 8.595 12c0 2.311 1.001 4.51 2.748 6.031zm5.241-13.447c-1.52 0-2.931.456-4.105 1.238.06.051.12.098.165.15C14.4 7.489 15.405 9.688 15.405 12c0 2.31-1.001 4.507-2.748 6.031-.058.049-.12.098-.181.146 1.177.783 2.588 1.238 4.107 1.238C20.68 19.416 24 16.096 24 12c0-4.094-3.32-7.416-7.416-7.416zM12 6.174c-.096.075-.189.15-.28.231C10.156 7.764 9.169 9.765 9.169 12c0 2.236.987 4.236 2.551 5.595.09.08.185.158.28.232.096-.074.189-.152.28-.232 1.563-1.359 2.551-3.359 2.551-5.595 0-2.235-.987-4.236-2.551-5.595-.09-.08-.184-.156-.28-.231z"/></svg>
|
||||||
|
After Width: | Height: | Size: 865 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="#FF4F40" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Monzo</title><path d="M4.244 1.174a.443.443 0 00-.271.13l-3.97 3.97-.001.001c3.884 3.882 8.093 8.092 11.748 11.748v-8.57L4.602 1.305a.443.443 0 00-.358-.131zm15.483 0a.443.443 0 00-.329.13L12.25 8.456v8.568L24 5.275c-1.316-1.322-2.647-2.648-3.97-3.97a.443.443 0 00-.301-.131zM0 5.979l.002 10.955c0 .294.118.577.326.785l4.973 4.976c.28.282.76.083.758-.314V12.037zm23.998.003l-6.06 6.061v10.338c-.004.399.48.6.76.314l4.974-4.976c.208-.208.326-.49.326-.785z"/></svg>
|
||||||
|
After Width: | Height: | Size: 556 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="#003087" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>PayPal</title><path d="M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 467 B |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="#0666EB" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Revolut</title><path d="M20.9133 6.9566C20.9133 3.1208 17.7898 0 13.9503 0H2.424v3.8605h10.9782c1.7376 0 3.177 1.3651 3.2087 3.043.016.84-.2994 1.633-.8878 2.2324-.5886.5998-1.375.9303-2.2144.9303H9.2322a.2756.2756 0 0 0-.2755.2752v3.431c0 .0585.018.1142.052.1612L16.2646 24h5.3114l-7.2727-10.094c3.6625-.1838 6.61-3.2612 6.61-6.9494zM6.8943 5.9229H2.424V24h4.4704z"/></svg>
|
||||||
|
After Width: | Height: | Size: 467 B |
@@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Santander"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 238.2 41.5"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="Banco_Santander_Logotipo.svg"
|
||||||
|
width="238.2"
|
||||||
|
height="41.5"
|
||||||
|
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
|
||||||
|
id="defs13" /><sodipodi:namedview
|
||||||
|
id="namedview11"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
showgrid="false"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:zoom="2.3575884"
|
||||||
|
inkscape:cx="105.82848"
|
||||||
|
inkscape:cy="46.445767"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1017"
|
||||||
|
inkscape:window-x="1912"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Santander" />
|
||||||
|
<style
|
||||||
|
type="text/css"
|
||||||
|
id="style2">
|
||||||
|
.st0{fill:#EA1D25;}
|
||||||
|
</style>
|
||||||
|
<title
|
||||||
|
id="title4">Santander</title>
|
||||||
|
<g
|
||||||
|
id="g8"
|
||||||
|
transform="translate(0,-0.4)">
|
||||||
|
<path
|
||||||
|
class="st0"
|
||||||
|
d="M 31.5,19.5 C 31.4,18 31,16.5 30.2,15.2 L 23.4,3.3 C 22.9,2.4 22.5,1.4 22.3,0.4 L 22,0.9 c -1.7,2.9 -1.7,6.6 0,9.5 l 5.5,9.5 c 1.7,2.9 1.7,6.6 0,9.5 l -0.3,0.5 c -0.2,-1 -0.6,-2 -1.1,-2.9 l -5,-8.7 -3.2,-5.6 C 17.4,11.8 17,10.8 16.8,9.8 l -0.3,0.5 c -1.7,2.9 -1.7,6.5 0,9.5 v 0 l 5.5,9.5 c 1.7,2.9 1.7,6.6 0,9.5 l -0.3,0.5 c -0.2,-1 -0.6,-2 -1.1,-2.9 L 13.7,24.5 C 12.8,22.9 12.4,21.1 12.4,19.3 5.1,21.2 0,25.3 0,30 0,36.6 9.8,41.9 21.9,41.9 34,41.9 43.8,36.6 43.8,30 43.9,25.5 38.9,21.4 31.5,19.5 Z m 20.7,20.3 c 0.1,-1.7 0.3,-2.8 0.8,-4.2 2.3,1.1 5.3,1.6 7.5,1.6 3.8,0 6,-1.2 6,-3.7 0,-2.4 -1.6,-3.5 -5.4,-5.2 L 59,27.5 c -3.9,-1.7 -6.5,-3.9 -6.5,-8.2 0,-4.7 3.3,-7.7 9.9,-7.7 2.7,0 5.2,0.4 7.5,1.2 -0.1,1.6 -0.4,2.9 -0.8,4.1 -2.2,-0.8 -4.9,-1.2 -6.8,-1.2 -3.6,0 -5.2,1.4 -5.2,3.6 0,2.1 1.6,3.4 4.5,4.6 l 2.2,0.9 c 5.2,2.2 7.4,4.6 7.4,8.6 0,4.7 -3.6,8 -10.6,8 -3.3,0 -6.1,-0.5 -8.4,-1.6 z M 93.3,20.1 v 20.6 h -4.2 l -0.2,-2.5 c -1.2,1.8 -2.9,3 -5.8,3 -5.4,0 -9.1,-4 -9.1,-10.9 0,-7.2 3.9,-11.4 11.5,-11.4 3,0.1 5.5,0.4 7.8,1.2 z M 88.8,36 V 23.1 c -0.9,-0.2 -2,-0.2 -3.3,-0.2 -4.7,0 -6.9,2.9 -6.9,7.5 0,4.2 1.7,7.2 5.7,7.2 1.9,-0.1 3.3,-0.7 4.5,-1.6 z m 27.7,-9.1 V 40.7 H 112 v -13 c 0,-3.3 -1.1,-4.8 -5.6,-4.8 -1.1,0 -2.3,0.1 -3.6,0.3 V 40.7 H 98.3 V 20.1 c 2.9,-0.7 6.1,-1.2 8.2,-1.2 7.6,0.1 10,3 10,8 z m 12.6,10.5 c 1.3,0 2.6,-0.2 3.5,-0.6 -0.1,1.2 -0.3,2.6 -0.5,3.8 -1.2,0.5 -2.6,0.7 -3.8,0.7 -4.4,0 -7.2,-2 -7.2,-7 V 12.6 c 1.4,-0.5 3.1,-0.7 4.5,-0.7 v 7.8 h 7.2 c -0.1,1.4 -0.2,2.7 -0.4,3.9 h -6.8 v 10.1 c 0,2.6 1.3,3.7 3.5,3.7 z m 24.3,-17.3 v 20.6 h -4.2 L 149,38.2 c -1.2,1.8 -2.9,3 -5.8,3 -5.4,0 -9.1,-4 -9.1,-10.9 0,-7.2 3.9,-11.4 11.5,-11.4 3,0.1 5.4,0.4 7.8,1.2 z M 148.8,36 V 23.1 c -0.9,-0.2 -2,-0.2 -3.3,-0.2 -4.7,0 -6.9,2.9 -6.9,7.5 0,4.2 1.7,7.2 5.7,7.2 1.9,-0.1 3.4,-0.7 4.5,-1.6 z m 27.8,-9.1 V 40.7 H 172 v -13 c 0,-3.3 -1.1,-4.8 -5.6,-4.8 -1.1,0 -2.3,0.1 -3.6,0.3 v 17.5 h -4.5 V 20.1 c 2.9,-0.7 6.1,-1.2 8.2,-1.2 7.6,0.1 10.1,3 10.1,8 z m 22.9,-15 v 28.8 h -4.2 l -0.2,-2.6 c -1.2,1.9 -2.9,3.1 -5.9,3.1 -5.4,0 -9.1,-4 -9.1,-10.9 0,-7.2 3.9,-11.4 11.5,-11.4 1.2,0 2.3,0.1 3.4,0.3 v -6.8 c 1.4,-0.4 3,-0.5 4.5,-0.5 z M 195,36 V 23.3 c -1.2,-0.2 -2.4,-0.4 -3.6,-0.4 -4.5,0 -6.6,2.8 -6.6,7.5 0,4.2 1.7,7.2 5.7,7.2 1.8,-0.1 3.3,-0.7 4.5,-1.6 z m 27.3,-4.1 h -14.5 c 0.6,3.7 2.7,5.4 6.8,5.4 2.5,0 5,-0.5 7.2,-1.6 -0.2,1.2 -0.4,2.8 -0.7,4.1 -2.1,0.9 -4.2,1.3 -6.7,1.3 -7.6,0 -11.2,-4.2 -11.2,-11.2 0,-6.1 2.8,-11 10,-11 6.5,0 9.3,4.2 9.3,9.4 0,1.4 0,2.4 -0.2,3.6 z M 207.8,28.1 H 218 c 0,-3.4 -1.8,-5.4 -4.9,-5.4 -3.3,0.1 -5,1.9 -5.3,5.4 z m 30.4,-8.9 c 0,1.4 -0.2,3 -0.4,3.9 -1.1,-0.1 -2.1,-0.2 -3.4,-0.2 -1.1,0 -2.2,0.1 -3.3,0.2 v 17.6 h -4.5 V 20.1 c 1.9,-0.7 5.2,-1.2 7.7,-1.2 1.3,0.1 2.9,0.1 3.9,0.3 z"
|
||||||
|
id="path6" />
|
||||||
|
</g>
|
||||||
|
<metadata
|
||||||
|
id="metadata830"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:title>Santander</dc:title></cc:Work></rdf:RDF></metadata></svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="#6935FF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Starling Bank</title><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm2.738 3.822h.666v2.724h-.666a4.794 4.794 0 0 0-4.789 4.788V12H7.226v-.666c0-4.142 3.37-7.512 7.512-7.512zM14.05 12h2.723v.666c0 4.142-3.37 7.512-7.512 7.512h-.666v-2.724h.666a4.794 4.794 0 0 0 4.789-4.788z"/></svg>
|
||||||
|
After Width: | Height: | Size: 409 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="#3C3CFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Tide</title><path d="M18.694 12.509h3.393c-.206-.846-.883-1.272-1.647-1.272-.883 0-1.5.48-1.746 1.272zm1.746 4.48c-2.238 0-3.679-1.57-3.679-3.648 0-2.024 1.501-3.662 3.693-3.662 2.211 0 3.546 1.532 3.546 3.569 0 .273-.027.626-.027.672h-5.346c.206.886.87 1.465 1.853 1.465.844 0 1.461-.366 1.853-.932l1.421.872c-.677 1.025-1.76 1.665-3.314 1.665m-6.179-3.634a1.89 1.89 0 00-1.906-1.884c-1.036 0-1.84.846-1.84 1.884 0 1.052.804 1.884 1.84 1.884 1.09 0 1.906-.832 1.906-1.884zm-.026 2.956c-.492.386-1.256.613-2.046.613a3.546 3.546 0 01-3.533-3.569c0-2.024 1.62-3.608 3.533-3.608.79 0 1.554.246 2.046.626v-2.91h1.892v9.368h-1.892v-.52M7.796 9.814H5.904v7.01h1.892v-7.01m-2.922 0v1.697H2.91v2.816c0 .626.285.872.93.872H4.88v1.625H3.706c-1.853 0-2.69-.832-2.69-2.404v-2.91H0V9.814a1.01 1.01 0 001.01-1.012V8.01h1.892v1.804h1.972m3.124-1.657c0 .632-.511 1.145-1.142 1.145-.63 0-1.142-.513-1.142-1.145 0-.633.511-1.145 1.142-1.145a1.135 1.135 0 011.142 1.145Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="500" height="181.82" viewBox="0 0 121 44" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.1028 22C44.1028 34.1505 34.2295 44 22.0514 44C9.87248 44 0 34.1505 0 22C0 9.84947 9.87248 0 22.0514 0C34.2295 0 44.1028 9.84947 44.1028 22Z" fill="#00A8E1"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.0981 14.1781H24.3201V33.8462H19.6417V14.1781H13.0047V10.1538H31.0981V14.1781Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M121 22C121 34.1505 111.127 44 98.949 44C86.7703 44 76.8972 34.1505 76.8972 22C76.8972 9.84947 86.7703 0 98.949 0C111.127 0 121 9.84947 121 22Z" fill="#001887"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M95.8729 23.0044V30.0136H99.994C102.549 30.0136 103.238 30.0136 104.352 29.2079C104.943 28.7726 105.402 27.8673 105.402 26.7251C105.408 26.1062 105.262 25.4949 104.976 24.9485C103.927 23.0376 101.502 23.0029 100.026 23.0029L95.8729 23.0044ZM95.8729 13.775V19.6812H97.2094C98.4268 19.6812 99.305 19.6812 99.8296 19.6481C100.911 19.6481 101.994 19.6141 102.781 19.1811C104.103 18.5645 104.686 16.9677 104.084 15.6141C103.853 15.0939 103.465 14.6625 102.978 14.3823C102.058 13.8452 101.6 13.8105 98.4547 13.7765L95.8729 13.775ZM91.5981 10.1538H99.697C102.778 10.187 103.599 10.2225 104.717 10.5559C107.071 11.242 108.689 13.4516 108.672 15.9575C108.672 16.9978 108.456 18.2736 107.799 19.1796C107.081 20.1528 106.162 20.6544 104.909 20.8929C107.466 21.3598 109.692 23.4783 109.692 27.1357C109.692 29.8531 108.515 32.2671 105.736 33.2766C104.261 33.8122 103.115 33.8462 100.494 33.8462H91.5981V10.1538Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M82.5514 22C82.5514 34.1505 72.6783 44 60.5004 44C48.3217 44 38.4486 34.1505 38.4486 22C38.4486 9.84947 48.3217 0 60.5004 0C72.6783 0 82.5514 9.84947 82.5514 22Z" fill="#0051B4"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.2757 32.7851C43.0758 29.5982 44.1028 25.9188 44.1028 22C44.1028 18.0812 43.0758 14.4018 41.2757 11.2149C39.4756 14.4018 38.4486 18.0812 38.4486 22C38.4486 25.9188 39.4756 29.5982 41.2757 32.7851Z" fill="#003974"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M79.7243 32.7851C81.5244 29.5983 82.5514 25.9188 82.5514 22C82.5514 18.0812 81.5244 14.4018 79.7243 11.2149C77.9242 14.4018 76.8972 18.0812 76.8972 22C76.8972 25.9188 77.9242 29.5983 79.7243 32.7851Z" fill="#131B33"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M62.8098 19.7949L60.4826 19.2601C58.4394 18.8578 56.3334 18.4555 56.3334 16.4441C56.3334 14.6412 58.3108 13.5676 60.5758 13.5676C63.9989 13.5676 65.5007 15.6767 66.2935 16.8898L69.6359 14.087C68.0305 11.867 65.517 9.58974 60.8013 9.58974C55.5037 9.58974 51.898 12.4646 51.898 16.9417C51.898 20.8522 54.674 22.2916 55.696 22.7295C56.3977 23.031 57.0988 23.1985 58.5319 23.5682L61.245 24.1681C63.2875 24.6719 63.3496 24.7061 63.862 24.9053C64.5653 25.2076 65.5547 25.7401 65.5547 27.2523C65.5547 28.0561 65.2648 29.2599 63.6395 29.9629C62.8401 30.2985 62.0119 30.3644 61.2147 30.3644C56.7002 30.3644 54.9469 27.9243 54.3205 26.9314L50.8878 29.6738C52.5813 32.4045 55.0578 33.4184 56.5212 33.8734C57.8323 34.2408 59.1848 34.4214 60.5418 34.4097C65.3262 34.4097 70.1121 32.2332 70.1121 26.8182C70.1166 21.7707 65.8446 20.4646 62.8098 19.7949Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 92 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="#1A1F71" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Visa</title><path d="M9.112 8.262L5.97 15.758H3.92L2.374 9.775c-.094-.368-.175-.503-.461-.658C1.447 8.864.677 8.627 0 8.479l.046-.217h3.3a.904.904 0 01.894.764l.817 4.338 2.018-5.102zm8.033 5.049c.008-1.979-2.736-2.088-2.717-2.972.006-.269.262-.555.822-.628a3.66 3.66 0 011.913.336l.34-1.59a5.207 5.207 0 00-1.814-.333c-1.917 0-3.266 1.02-3.278 2.479-.012 1.079.963 1.68 1.698 2.04.756.367 1.01.603 1.006.931-.005.504-.602.725-1.16.734-.975.015-1.54-.263-1.992-.473l-.351 1.642c.453.208 1.289.39 2.156.398 2.037 0 3.37-1.006 3.377-2.564m5.061 2.447H24l-1.565-7.496h-1.656a.883.883 0 00-.826.55l-2.909 6.946h2.036l.405-1.12h2.488zm-2.163-2.656l1.02-2.815.588 2.815zm-8.16-4.84l-1.603 7.496H8.34l1.605-7.496z"/></svg>
|
||||||
|
After Width: | Height: | Size: 808 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="#163300" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Wise</title><path d="M6.488 7.469 0 15.05h11.585l1.301-3.576H7.922l3.033-3.507.01-.092L8.993 4.48h8.873l-6.878 18.925h4.706L24 .595H2.543l3.945 6.874Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,30 @@
|
|||||||
|
// LibreLedger — pure-JS crypto for pages without Web Crypto.
|
||||||
|
//
|
||||||
|
// Browsers only expose crypto.subtle in a secure context (HTTPS or localhost).
|
||||||
|
// Opened as plain http://<LAN or VPN address>:<port>, the app loads this module
|
||||||
|
// instead. It does exactly what the Web Crypto path does, so the two read and
|
||||||
|
// write the same ledger files:
|
||||||
|
// key = PBKDF2-HMAC-SHA256(UTF-8 passphrase, 16-byte salt, iterations) -> 32 bytes
|
||||||
|
// ct = AES-256-GCM(key, 12-byte iv, plaintext) -> ciphertext || 16-byte tag
|
||||||
|
// The primitives are unmodified copies of @noble/hashes and @noble/ciphers
|
||||||
|
// (audited, zero-dependency); versions and hashes are in vendor/README.md.
|
||||||
|
//
|
||||||
|
// The one real difference: Web Crypto keys are non-extractable, while here the
|
||||||
|
// 32 key bytes live in page memory until the ledger is locked. Use HTTPS where
|
||||||
|
// you can.
|
||||||
|
import { pbkdf2Async } from "./vendor/noble-hashes-2.2.0/pbkdf2.js";
|
||||||
|
import { sha256 } from "./vendor/noble-hashes-2.2.0/sha2.js";
|
||||||
|
import { gcm } from "./vendor/noble-ciphers-2.2.0/aes.js";
|
||||||
|
|
||||||
|
export async function deriveKeyBytes(passphraseBytes, salt, iterations) {
|
||||||
|
return pbkdf2Async(sha256, passphraseBytes, salt, { c: iterations, dkLen: 32, asyncTick: 25 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encrypt(keyBytes, iv, plaintext) {
|
||||||
|
return gcm(keyBytes, iv).encrypt(plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throws on a wrong key or tampered data, like crypto.subtle.decrypt does.
|
||||||
|
export function decrypt(keyBytes, iv, ciphertext) {
|
||||||
|
return gcm(keyBytes, iv).decrypt(ciphertext);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#1f6f5c"/><rect x="14" y="14" width="36" height="40" rx="4" fill="#f4f1e8"/><path d="M20 24h24M20 32h24M20 40h16" stroke="#1f6f5c" stroke-width="3" stroke-linecap="round"/><circle cx="44" cy="44" r="9" fill="#e0a526"/><path d="M44 39v10M41 42h5" stroke="#1f2a26" stroke-width="2" stroke-linecap="round"/></svg>
|
||||||
|
After Width: | Height: | Size: 414 B |
@@ -0,0 +1,112 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="color-scheme" content="light dark">
|
||||||
|
<title>LibreLedger</title>
|
||||||
|
<link rel="icon" href="favicon.svg" type="image/svg+xml">
|
||||||
|
<script src="theme.js"></script>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Lock / unlock screen -->
|
||||||
|
<div id="lock" class="overlay">
|
||||||
|
<form id="lock-form" class="lock-box">
|
||||||
|
<div class="brand brand-lg">
|
||||||
|
<span class="brand-mark">£</span>
|
||||||
|
<span class="brand-name">LibreLedger</span>
|
||||||
|
</div>
|
||||||
|
<p id="lock-msg">Enter your passphrase to unlock</p>
|
||||||
|
<input type="password" id="pass1" placeholder="Passphrase" autocomplete="current-password">
|
||||||
|
<input type="password" id="pass2" placeholder="Confirm passphrase" autocomplete="new-password" hidden>
|
||||||
|
<button type="submit" id="lock-btn">Unlock</button>
|
||||||
|
<p id="lock-err" class="err"></p>
|
||||||
|
<p id="lock-warn" class="lock-warn" hidden></p>
|
||||||
|
<div id="lock-fs" class="lock-fs"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div class="brand">
|
||||||
|
<span class="brand-mark">£</span>
|
||||||
|
<span class="brand-name">LibreLedger</span>
|
||||||
|
</div>
|
||||||
|
<div class="tools">
|
||||||
|
<label class="cur">Currency
|
||||||
|
<input id="currency" maxlength="3" value="£">
|
||||||
|
</label>
|
||||||
|
<button id="btn-theme" type="button" class="icon-btn" title="Toggle dark / light theme" aria-label="Toggle theme"></button>
|
||||||
|
<div class="menu" id="tools-menu">
|
||||||
|
<button type="button" class="menu-btn" id="btn-more" aria-haspopup="true" aria-expanded="false" title="More options">⋯</button>
|
||||||
|
<div class="menu-pop" role="menu" hidden>
|
||||||
|
<button id="btn-linkfile" type="button" hidden>🔗 Link file</button>
|
||||||
|
<button id="btn-csv" type="button">Export CSV</button>
|
||||||
|
<button id="btn-pdf" type="button">Export PDF</button>
|
||||||
|
<button id="btn-backup" type="button">Backup</button>
|
||||||
|
<button id="btn-restore" type="button">Restore</button>
|
||||||
|
<button id="btn-passphrase" type="button">Change passphrase</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="file-restore" accept="application/json,.mlg" hidden>
|
||||||
|
<button id="btn-lock" type="button" class="primary">Lock</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="app" class="hidden">
|
||||||
|
<div id="budget-bar" class="budget-bar"></div>
|
||||||
|
<div id="section-nav" class="section-nav"></div>
|
||||||
|
|
||||||
|
<section class="panel" data-section="totals">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>📊 Totals</h2>
|
||||||
|
<p class="panel-sub">Your money at a glance — annualised over the <em>next 12 months</em> from your recurring schedule. Click any figure to chart it month by month.</p>
|
||||||
|
</div>
|
||||||
|
<div id="totals-body"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel" data-section="savings">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>🐷 Savings</h2>
|
||||||
|
<p class="panel-sub">Every 🐷 Savings-tagged row across your <em>whole ledger</em>, added up month by month. (The 📊 Totals card above counts only the next 12 months, so a longer ledger totals more here.)</p>
|
||||||
|
</div>
|
||||||
|
<div id="savings-body"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel" data-section="affordability">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>🏡 Housing affordability</h2>
|
||||||
|
<p class="panel-sub">Your 🏠 Housing costs measured against your 💰 Income — based on the 30% rule of thumb.</p>
|
||||||
|
</div>
|
||||||
|
<div id="afford-body"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel" data-section="balances">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>💼 Balances</h2>
|
||||||
|
<p class="panel-sub">Where your money lives right now — this total kicks off the ledger 👇</p>
|
||||||
|
</div>
|
||||||
|
<div id="accounts-body"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel" data-section="recurring">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>🔁 Recurring</h2>
|
||||||
|
<p class="panel-sub">Bills & income that repeat — auto-added to new months, or drop into any month on demand.</p>
|
||||||
|
</div>
|
||||||
|
<div id="recurring-body"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel" data-section="ledger">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>🌊 Monthly ledger</h2>
|
||||||
|
<p class="panel-sub">Every penny in, every penny out — one smooth balance flowing month to month.</p>
|
||||||
|
</div>
|
||||||
|
<div id="months-body"></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="app.js" defer></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""LibreLedger — tiny persistence server (Python standard library only).
|
||||||
|
|
||||||
|
Serves the app's own files AND stores the *already client-side-encrypted*
|
||||||
|
ledger blob in a file on disk: <data-dir>/ledger.enc
|
||||||
|
|
||||||
|
The browser does all the encryption (AES-256-GCM, key from PBKDF2). This
|
||||||
|
server only ever stores and returns the opaque ciphertext; it never sees the
|
||||||
|
passphrase, the key or a single figure from the ledger.
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
GET /api/data -> 200 + encrypted blob, or 204 if nothing stored yet
|
||||||
|
PUT /api/data -> store the encrypted blob (checked to look like one)
|
||||||
|
GET /api/health -> 200 {"ok":true}
|
||||||
|
GET /<app file> -> one of the files in APP_FILES / banks/*.svg / VENDOR_FILES
|
||||||
|
everything else -> 404 (no directory listings, never data/ or this source)
|
||||||
|
|
||||||
|
Every save keeps the previous version under <data-dir>/backups/:
|
||||||
|
the newest BACKUP_KEEP_RECENT snapshots, plus the last snapshot of each of the
|
||||||
|
most recent BACKUP_KEEP_DAILY days. At most one snapshot is taken per
|
||||||
|
BACKUP_INTERVAL seconds, so typing does not churn through the recent ones.
|
||||||
|
|
||||||
|
Run: python3 server.py [--port 8080] [--host 127.0.0.1] [--data-dir ./data]
|
||||||
|
Open: http://localhost:8080
|
||||||
|
Every option can also be set in the environment: LIBRELEDGER_PORT,
|
||||||
|
LIBRELEDGER_HOST, LIBRELEDGER_DATA_DIR, LIBRELEDGER_BACKUP_INTERVAL,
|
||||||
|
LIBRELEDGER_BACKUP_KEEP_RECENT, LIBRELEDGER_BACKUP_KEEP_DAILY.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from urllib.parse import urlsplit, unquote
|
||||||
|
|
||||||
|
VERSION = "1.0.0"
|
||||||
|
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
MAX_BODY = 16 * 1024 * 1024 # a real ledger is tens of KB
|
||||||
|
|
||||||
|
# The only files this server hands out. Nothing else under ROOT is reachable:
|
||||||
|
# not data/, not this source, not the Dockerfile or the tests.
|
||||||
|
APP_FILES = {
|
||||||
|
"index.html": "text/html; charset=utf-8",
|
||||||
|
"app.js": "text/javascript; charset=utf-8",
|
||||||
|
"theme.js": "text/javascript; charset=utf-8",
|
||||||
|
"crypto-fallback.js": "text/javascript; charset=utf-8",
|
||||||
|
"styles.css": "text/css; charset=utf-8",
|
||||||
|
"favicon.svg": "image/svg+xml",
|
||||||
|
}
|
||||||
|
# The pure-JS crypto used when the browser has no Web Crypto (plain http on a
|
||||||
|
# LAN or VPN address). Byte-for-byte copies of the npm releases; see vendor/README.md.
|
||||||
|
VENDOR_FILES = {
|
||||||
|
"vendor/noble-hashes-2.2.0/pbkdf2.js",
|
||||||
|
"vendor/noble-hashes-2.2.0/hmac.js",
|
||||||
|
"vendor/noble-hashes-2.2.0/sha2.js",
|
||||||
|
"vendor/noble-hashes-2.2.0/_md.js",
|
||||||
|
"vendor/noble-hashes-2.2.0/_u64.js",
|
||||||
|
"vendor/noble-hashes-2.2.0/utils.js",
|
||||||
|
"vendor/noble-ciphers-2.2.0/aes.js",
|
||||||
|
"vendor/noble-ciphers-2.2.0/_polyval.js",
|
||||||
|
"vendor/noble-ciphers-2.2.0/utils.js",
|
||||||
|
}
|
||||||
|
BANK_SVG = re.compile(r"^banks/[a-z0-9]+\.svg$")
|
||||||
|
|
||||||
|
# Everything is same-origin; the page loads nothing from anywhere else.
|
||||||
|
# style-src-attr: the app sets a few colours through style="--dot:…" attributes.
|
||||||
|
CSP = ("default-src 'none'; script-src 'self'; style-src 'self'; "
|
||||||
|
"style-src-attr 'unsafe-inline'; img-src 'self'; connect-src 'self'; "
|
||||||
|
"base-uri 'none'; form-action 'self'; frame-ancestors 'none'")
|
||||||
|
SECURITY_HEADERS = (
|
||||||
|
("Content-Security-Policy", CSP),
|
||||||
|
("X-Content-Type-Options", "nosniff"),
|
||||||
|
("X-Frame-Options", "DENY"),
|
||||||
|
("Referrer-Policy", "no-referrer"),
|
||||||
|
("Cross-Origin-Opener-Policy", "same-origin"),
|
||||||
|
("Cross-Origin-Resource-Policy", "same-origin"),
|
||||||
|
("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=()"),
|
||||||
|
)
|
||||||
|
|
||||||
|
SAVE_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _env_int(name, default):
|
||||||
|
try:
|
||||||
|
return int(os.environ.get(name, default))
|
||||||
|
except ValueError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
class Store:
|
||||||
|
"""The ledger file and its rotating backups."""
|
||||||
|
|
||||||
|
def __init__(self, data_dir, interval, keep_recent, keep_daily):
|
||||||
|
self.dir = os.path.abspath(data_dir)
|
||||||
|
self.file = os.path.join(self.dir, "ledger.enc")
|
||||||
|
self.backups = os.path.join(self.dir, "backups")
|
||||||
|
self.interval = max(0, interval)
|
||||||
|
self.keep_recent = max(1, keep_recent)
|
||||||
|
self.keep_daily = max(0, keep_daily)
|
||||||
|
os.makedirs(self.backups, exist_ok=True)
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
try:
|
||||||
|
with open(self.file, "rb") as f:
|
||||||
|
return f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _snapshots(self):
|
||||||
|
names = [n for n in os.listdir(self.backups)
|
||||||
|
if n.startswith("ledger-") and n.endswith(".enc")]
|
||||||
|
return sorted(names) # the timestamp format sorts chronologically
|
||||||
|
|
||||||
|
def _backup_current(self):
|
||||||
|
if not os.path.exists(self.file):
|
||||||
|
return
|
||||||
|
snaps = self._snapshots()
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
if snaps and self.interval:
|
||||||
|
try:
|
||||||
|
last = datetime.datetime.strptime(snaps[-1][7:-4], "%Y%m%dT%H%M%S%fZ")
|
||||||
|
last = last.replace(tzinfo=datetime.timezone.utc)
|
||||||
|
if (now - last).total_seconds() < self.interval:
|
||||||
|
return
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
name = "ledger-" + now.strftime("%Y%m%dT%H%M%S%fZ") + ".enc"
|
||||||
|
self._write_atomic(os.path.join(self.backups, name), self.read())
|
||||||
|
self._prune()
|
||||||
|
|
||||||
|
def _prune(self):
|
||||||
|
snaps = self._snapshots()
|
||||||
|
keep = set(snaps[-self.keep_recent:])
|
||||||
|
days = {}
|
||||||
|
for n in snaps: # later names overwrite earlier: the last one of each day wins
|
||||||
|
days[n[7:15]] = n
|
||||||
|
for day in sorted(days)[-self.keep_daily:] if self.keep_daily else []:
|
||||||
|
keep.add(days[day])
|
||||||
|
for n in snaps:
|
||||||
|
if n not in keep:
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(self.backups, n))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _write_atomic(self, path, body):
|
||||||
|
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), prefix=".ledger-", suffix=".tmp")
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, "wb") as f:
|
||||||
|
f.write(body)
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
os.chmod(tmp, 0o600)
|
||||||
|
os.replace(tmp, path)
|
||||||
|
dfd = os.open(os.path.dirname(path), os.O_RDONLY)
|
||||||
|
try:
|
||||||
|
os.fsync(dfd)
|
||||||
|
finally:
|
||||||
|
os.close(dfd)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(tmp):
|
||||||
|
os.remove(tmp)
|
||||||
|
|
||||||
|
def save(self, body):
|
||||||
|
with SAVE_LOCK:
|
||||||
|
try:
|
||||||
|
self._backup_current()
|
||||||
|
except OSError as e: # a failed backup must not block the save
|
||||||
|
print(f"backup failed: {e}", file=sys.stderr)
|
||||||
|
self._write_atomic(self.file, body)
|
||||||
|
|
||||||
|
|
||||||
|
def _b64len(value):
|
||||||
|
"""Decoded length of a strict base64 string, or -1."""
|
||||||
|
if not isinstance(value, str) or len(value) > MAX_BODY * 2:
|
||||||
|
return -1
|
||||||
|
try:
|
||||||
|
return len(base64.b64decode(value, validate=True))
|
||||||
|
except (binascii.Error, ValueError):
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
def valid_blob(body):
|
||||||
|
"""True for {v?, salt, iv, ct} as the app writes it, and nothing else."""
|
||||||
|
try:
|
||||||
|
obj = json.loads(body)
|
||||||
|
except (ValueError, UnicodeDecodeError):
|
||||||
|
return False
|
||||||
|
if not isinstance(obj, dict) or not {"salt", "iv", "ct"} <= obj.keys():
|
||||||
|
return False
|
||||||
|
if set(obj) - {"v", "salt", "iv", "ct"}:
|
||||||
|
return False
|
||||||
|
if "v" in obj and (not isinstance(obj["v"], int) or isinstance(obj["v"], bool)):
|
||||||
|
return False
|
||||||
|
return (_b64len(obj["salt"]) == 16 and _b64len(obj["iv"]) == 12
|
||||||
|
and _b64len(obj["ct"]) >= 16)
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
store = None # set in main()
|
||||||
|
|
||||||
|
def version_string(self):
|
||||||
|
return "LibreLedger"
|
||||||
|
|
||||||
|
def log_message(self, fmt, *args):
|
||||||
|
# No client addresses in the log; the request line and status are enough.
|
||||||
|
sys.stderr.write("%s %s\n" % (self.log_date_time_string(), fmt % args))
|
||||||
|
|
||||||
|
def _send(self, code, body=b"", ctype="application/json", cache="no-store", head=False):
|
||||||
|
self.send_response(code)
|
||||||
|
for k, v in SECURITY_HEADERS:
|
||||||
|
self.send_header(k, v)
|
||||||
|
if code != 204:
|
||||||
|
self.send_header("Content-Type", ctype)
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.send_header("Cache-Control", cache)
|
||||||
|
self.end_headers()
|
||||||
|
if body and not head and code != 204:
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def _path(self):
|
||||||
|
return unquote(urlsplit(self.path).path)
|
||||||
|
|
||||||
|
def _static(self, path, head):
|
||||||
|
rel = "index.html" if path == "/" else path.lstrip("/")
|
||||||
|
if rel in APP_FILES:
|
||||||
|
ctype, cache = APP_FILES[rel], "no-cache"
|
||||||
|
elif rel in VENDOR_FILES:
|
||||||
|
ctype, cache = "text/javascript; charset=utf-8", "public, max-age=31536000, immutable"
|
||||||
|
elif BANK_SVG.match(rel):
|
||||||
|
ctype, cache = "image/svg+xml", "public, max-age=86400"
|
||||||
|
else:
|
||||||
|
return self._send(404, b'{"error":"not found"}', head=head)
|
||||||
|
try:
|
||||||
|
with open(os.path.join(ROOT, rel), "rb") as f:
|
||||||
|
body = f.read()
|
||||||
|
except OSError:
|
||||||
|
return self._send(404, b'{"error":"not found"}', head=head)
|
||||||
|
return self._send(200, body, ctype, cache, head=head)
|
||||||
|
|
||||||
|
def _get(self, head):
|
||||||
|
path = self._path()
|
||||||
|
if path == "/api/health":
|
||||||
|
return self._send(200, b'{"ok":true}', head=head)
|
||||||
|
if path == "/api/data":
|
||||||
|
body = self.store.read()
|
||||||
|
if body is None:
|
||||||
|
return self._send(204, head=head)
|
||||||
|
return self._send(200, body, head=head)
|
||||||
|
return self._static(path, head)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
self._get(head=False)
|
||||||
|
|
||||||
|
def do_HEAD(self):
|
||||||
|
self._get(head=True)
|
||||||
|
|
||||||
|
def _cross_site(self):
|
||||||
|
# A write must come from this app's own page. Browsers send these on
|
||||||
|
# cross-origin requests; a JSON PUT from elsewhere is also stopped by
|
||||||
|
# CORS preflight, since this server never answers one.
|
||||||
|
if self.headers.get("Sec-Fetch-Site", "same-origin") not in ("same-origin", "none"):
|
||||||
|
return True
|
||||||
|
origin = self.headers.get("Origin")
|
||||||
|
if origin:
|
||||||
|
host = self.headers.get("X-Forwarded-Host") or self.headers.get("Host") or ""
|
||||||
|
if urlsplit(origin).netloc.lower() != host.split(",")[0].strip().lower():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def do_PUT(self):
|
||||||
|
if self._path() != "/api/data":
|
||||||
|
return self._send(405, b'{"error":"method not allowed"}')
|
||||||
|
if self._cross_site():
|
||||||
|
return self._send(403, b'{"error":"cross-site request refused"}')
|
||||||
|
ctype = (self.headers.get("Content-Type") or "").split(";")[0].strip().lower()
|
||||||
|
if ctype != "application/json":
|
||||||
|
return self._send(415, b'{"error":"expected application/json"}')
|
||||||
|
try:
|
||||||
|
length = int(self.headers.get("Content-Length") or 0)
|
||||||
|
except ValueError:
|
||||||
|
length = -1
|
||||||
|
if length <= 0 or length > MAX_BODY:
|
||||||
|
self.close_connection = True
|
||||||
|
return self._send(413 if length > MAX_BODY else 400, b'{"error":"bad content-length"}')
|
||||||
|
body = self.rfile.read(length)
|
||||||
|
# Only accept something shaped like our encrypted blob; never store junk.
|
||||||
|
if not valid_blob(body):
|
||||||
|
return self._send(400, b'{"error":"not an encrypted ledger blob"}')
|
||||||
|
try:
|
||||||
|
self.store.save(body)
|
||||||
|
except OSError as e:
|
||||||
|
print(f"save failed: {e}", file=sys.stderr)
|
||||||
|
return self._send(500, b'{"error":"could not write the ledger file"}')
|
||||||
|
return self._send(200, b'{"ok":true}')
|
||||||
|
|
||||||
|
def _refuse(self):
|
||||||
|
self._send(405, b'{"error":"method not allowed"}')
|
||||||
|
|
||||||
|
do_POST = do_DELETE = do_PATCH = do_OPTIONS = _refuse
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="LibreLedger server")
|
||||||
|
ap.add_argument("--port", type=int, default=_env_int("LIBRELEDGER_PORT", 8080))
|
||||||
|
ap.add_argument("--host", default=os.environ.get("LIBRELEDGER_HOST", "127.0.0.1"),
|
||||||
|
help="address to listen on (default 127.0.0.1, this machine only)")
|
||||||
|
ap.add_argument("--data-dir", default=os.environ.get("LIBRELEDGER_DATA_DIR",
|
||||||
|
os.path.join(ROOT, "data")),
|
||||||
|
help="where ledger.enc and backups/ live (default ./data)")
|
||||||
|
ap.add_argument("--backup-interval", type=int,
|
||||||
|
default=_env_int("LIBRELEDGER_BACKUP_INTERVAL", 600),
|
||||||
|
help="minimum seconds between backup snapshots (default 600, 0 = every save)")
|
||||||
|
ap.add_argument("--backup-keep-recent", type=int,
|
||||||
|
default=_env_int("LIBRELEDGER_BACKUP_KEEP_RECENT", 10))
|
||||||
|
ap.add_argument("--backup-keep-daily", type=int,
|
||||||
|
default=_env_int("LIBRELEDGER_BACKUP_KEEP_DAILY", 30))
|
||||||
|
args = ap.parse_args()
|
||||||
|
os.umask(0o077)
|
||||||
|
Handler.store = Store(args.data_dir, args.backup_interval,
|
||||||
|
args.backup_keep_recent, args.backup_keep_daily)
|
||||||
|
httpd = ThreadingHTTPServer((args.host, args.port), Handler)
|
||||||
|
httpd.daemon_threads = True
|
||||||
|
print(f"LibreLedger {VERSION} -> http://{args.host}:{args.port}", flush=True)
|
||||||
|
print(f"Encrypted data file: {Handler.store.file}", flush=True)
|
||||||
|
try:
|
||||||
|
httpd.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
httpd.server_close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
// Proves the pure-JS fallback (crypto-fallback.js) and Web Crypto read and
|
||||||
|
// write the same ledger blobs, in both directions, with the app's parameters.
|
||||||
|
//
|
||||||
|
// node tests/crypto-interop.test.mjs (Node 22.12 or newer)
|
||||||
|
// docker run --rm --network none -v "$PWD":/src:ro -w /src \
|
||||||
|
// node:24-alpine@sha256:50c8e8ca1d27439048670df5883f32d57cf81cff6233222c893fd0d9884cbd81 \
|
||||||
|
// node tests/crypto-interop.test.mjs
|
||||||
|
//
|
||||||
|
// Node's globalThis.crypto.subtle is the same Web Crypto API browsers expose.
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import * as fb from "../crypto-fallback.js";
|
||||||
|
|
||||||
|
const subtle = globalThis.crypto.subtle;
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
const dec = new TextDecoder();
|
||||||
|
const hex = b => Buffer.from(b).toString("hex");
|
||||||
|
const b64e = b => Buffer.from(b).toString("base64");
|
||||||
|
const b64d = s => new Uint8Array(Buffer.from(s, "base64"));
|
||||||
|
let passed = 0;
|
||||||
|
async function test(name, fn) {
|
||||||
|
const t = Date.now();
|
||||||
|
await fn();
|
||||||
|
passed++;
|
||||||
|
console.log(`ok ${name} (${Date.now() - t} ms)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The parameters must be the app's own, not a copy that could drift.
|
||||||
|
const appSrc = readFileSync(new URL("../app.js", import.meta.url), "utf8");
|
||||||
|
const ITER = Number(/const PBKDF2_ITERATIONS = (\d+);/.exec(appSrc)[1]);
|
||||||
|
assert.equal(ITER, 250000);
|
||||||
|
assert.match(appSrc, /name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256"/);
|
||||||
|
assert.match(appSrc, /\{ name: "AES-GCM", length: 256 \}/);
|
||||||
|
assert.match(appSrc, /crypto\.getRandomValues\(new Uint8Array\(12\)\)/);
|
||||||
|
assert.match(appSrc, /crypto\.getRandomValues\(new Uint8Array\(16\)\)/);
|
||||||
|
|
||||||
|
// --- Web Crypto side, written exactly as app.js does it -----------------
|
||||||
|
async function wcKey(pass, salt) {
|
||||||
|
const km = await subtle.importKey("raw", enc.encode(pass), "PBKDF2", false, ["deriveKey"]);
|
||||||
|
return subtle.deriveKey({ name: "PBKDF2", salt, iterations: ITER, hash: "SHA-256" },
|
||||||
|
km, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
||||||
|
}
|
||||||
|
async function wcEncrypt(obj, pass) {
|
||||||
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
const ct = await subtle.encrypt({ name: "AES-GCM", iv }, await wcKey(pass, salt), enc.encode(JSON.stringify(obj)));
|
||||||
|
return { v: 1, salt: b64e(salt), iv: b64e(iv), ct: b64e(ct) };
|
||||||
|
}
|
||||||
|
async function wcDecrypt(blob, pass) {
|
||||||
|
const pt = await subtle.decrypt({ name: "AES-GCM", iv: b64d(blob.iv) }, await wcKey(pass, b64d(blob.salt)), b64d(blob.ct));
|
||||||
|
return JSON.parse(dec.decode(pt));
|
||||||
|
}
|
||||||
|
// --- fallback side -------------------------------------------------------
|
||||||
|
async function fbEncrypt(obj, pass) {
|
||||||
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
const key = await fb.deriveKeyBytes(enc.encode(pass), salt, ITER);
|
||||||
|
return { v: 1, salt: b64e(salt), iv: b64e(iv), ct: b64e(fb.encrypt(key, iv, enc.encode(JSON.stringify(obj)))) };
|
||||||
|
}
|
||||||
|
async function fbDecrypt(blob, pass) {
|
||||||
|
const key = await fb.deriveKeyBytes(enc.encode(pass), b64d(blob.salt), ITER);
|
||||||
|
return JSON.parse(dec.decode(fb.decrypt(key, b64d(blob.iv), b64d(blob.ct))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fake data only.
|
||||||
|
const sample = {
|
||||||
|
version: 2, activeId: "b1",
|
||||||
|
budgets: [{ id: "b1", name: "Test budget £€", currency: "£",
|
||||||
|
accounts: [{ id: "a1", name: "Example Bank", balance: "123.45", bank: "cash" }],
|
||||||
|
recurring: [], months: [{ id: "m1", ym: "2026-09", rows: [
|
||||||
|
{ id: "r1", date: "2026-09-01", desc: "Fake coffee ☕", inc: "", out: "3.20", tag: "" }] }] }],
|
||||||
|
};
|
||||||
|
|
||||||
|
await test("PBKDF2-HMAC-SHA256 known answer (c=1, RFC 7914 §11 style)", async () => {
|
||||||
|
const k = await fb.deriveKeyBytes(enc.encode("password"), enc.encode("salt"), 1);
|
||||||
|
assert.equal(hex(k), "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test("AES-256-GCM known answer (GCM spec test case 14)", async () => {
|
||||||
|
const ct = fb.encrypt(new Uint8Array(32), new Uint8Array(12), new Uint8Array(16));
|
||||||
|
assert.equal(hex(ct), "cea7403d4d606b6e074ec5d3baf39d18" + "d0d1c8a799996bf0265b98b5d48ab919");
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const pass of ["correct horse battery staple", "pässwörd ✓ 🔐", "x"]) {
|
||||||
|
await test(`derived key bytes match Web Crypto (${JSON.stringify(pass)}, ${ITER} iterations)`, async () => {
|
||||||
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
const km = await subtle.importKey("raw", enc.encode(pass), "PBKDF2", false, ["deriveBits"]);
|
||||||
|
const bits = await subtle.deriveBits({ name: "PBKDF2", salt, iterations: ITER, hash: "SHA-256" }, km, 256);
|
||||||
|
assert.equal(hex(await fb.deriveKeyBytes(enc.encode(pass), salt, ITER)), hex(bits));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await test("Web Crypto blob decrypts with the fallback", async () => {
|
||||||
|
const blob = await wcEncrypt(sample, "throwaway-test-pass");
|
||||||
|
assert.deepEqual(Object.keys(blob), ["v", "salt", "iv", "ct"]);
|
||||||
|
assert.deepEqual(await fbDecrypt(blob, "throwaway-test-pass"), sample);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test("fallback blob decrypts with Web Crypto", async () => {
|
||||||
|
const blob = await fbEncrypt(sample, "throwaway-test-pass");
|
||||||
|
assert.deepEqual(await wcDecrypt(blob, "throwaway-test-pass"), sample);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test("same key, iv and plaintext give byte-identical ciphertext", async () => {
|
||||||
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
const pt = enc.encode(JSON.stringify(sample));
|
||||||
|
const a = new Uint8Array(await subtle.encrypt({ name: "AES-GCM", iv }, await wcKey("p", salt), pt));
|
||||||
|
const b = fb.encrypt(await fb.deriveKeyBytes(enc.encode("p"), salt, ITER), iv, pt);
|
||||||
|
assert.equal(hex(b), hex(a));
|
||||||
|
});
|
||||||
|
|
||||||
|
await test("fallback rejects a wrong passphrase and tampered data", async () => {
|
||||||
|
const blob = await wcEncrypt(sample, "right");
|
||||||
|
await assert.rejects(fbDecrypt(blob, "wrong"));
|
||||||
|
const ct = b64d(blob.ct); ct[0] ^= 1;
|
||||||
|
await assert.rejects(fbDecrypt({ ...blob, ct: b64e(ct) }, "right"));
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\n${passed} passed`);
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""server.py: file allowlist, headers, blob checks, atomic saves, backups.
|
||||||
|
|
||||||
|
python3 tests/test_server.py
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import datetime
|
||||||
|
import http.client
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import unittest
|
||||||
|
from http.server import ThreadingHTTPServer
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import server # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def blob(ct_len=40):
|
||||||
|
b = lambda n: base64.b64encode(os.urandom(n)).decode()
|
||||||
|
return json.dumps({"v": 1, "salt": b(16), "iv": b(12), "ct": b(ct_len)}).encode()
|
||||||
|
|
||||||
|
|
||||||
|
class ServerTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
server.Handler.store = server.Store(self.tmp.name, 0, 3, 2)
|
||||||
|
self.httpd = ThreadingHTTPServer(("127.0.0.1", 0), server.Handler)
|
||||||
|
threading.Thread(target=self.httpd.serve_forever, daemon=True).start()
|
||||||
|
self.port = self.httpd.server_address[1]
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.httpd.shutdown()
|
||||||
|
self.httpd.server_close()
|
||||||
|
self.tmp.cleanup()
|
||||||
|
|
||||||
|
def req(self, method, path, body=None, headers=None):
|
||||||
|
c = http.client.HTTPConnection("127.0.0.1", self.port, timeout=5)
|
||||||
|
h = {"Content-Type": "application/json"} if body is not None else {}
|
||||||
|
h.update(headers or {})
|
||||||
|
c.request(method, path, body=body, headers=h)
|
||||||
|
r = c.getresponse()
|
||||||
|
data = r.read()
|
||||||
|
c.close()
|
||||||
|
return r, data
|
||||||
|
|
||||||
|
def test_health_and_headers(self):
|
||||||
|
r, data = self.req("GET", "/api/health")
|
||||||
|
self.assertEqual((r.status, data), (200, b'{"ok":true}'))
|
||||||
|
csp = r.getheader("Content-Security-Policy")
|
||||||
|
self.assertIn("default-src 'none'", csp)
|
||||||
|
self.assertIn("frame-ancestors 'none'", csp)
|
||||||
|
self.assertEqual(r.getheader("X-Content-Type-Options"), "nosniff")
|
||||||
|
self.assertEqual(r.getheader("X-Frame-Options"), "DENY")
|
||||||
|
self.assertEqual(r.getheader("Referrer-Policy"), "no-referrer")
|
||||||
|
|
||||||
|
def test_serves_only_app_files(self):
|
||||||
|
for path in ("/", "/index.html", "/app.js", "/theme.js", "/styles.css",
|
||||||
|
"/crypto-fallback.js", "/banks/monzo.svg",
|
||||||
|
"/vendor/noble-hashes-2.2.0/pbkdf2.js", "/vendor/noble-ciphers-2.2.0/aes.js"):
|
||||||
|
r, _ = self.req("GET", path)
|
||||||
|
self.assertEqual(r.status, 200, path)
|
||||||
|
r, _ = self.req("GET", "/app.js")
|
||||||
|
self.assertTrue(r.getheader("Content-Type").startswith("text/javascript"))
|
||||||
|
for path in ("/server.py", "/Dockerfile", "/README.md", "/LICENSE", "/data/",
|
||||||
|
"/data/ledger.enc", "/banks/", "/banks/NOTICE.txt", "/vendor/",
|
||||||
|
"/vendor/README.md", "/vendor/noble-hashes-2.2.0/LICENSE",
|
||||||
|
"/vendor/noble-hashes-2.2.0/argon2.js", "/tests/test_server.py",
|
||||||
|
"/../server.py", "/%2e%2e/server.py", "/banks/..%2fserver.py",
|
||||||
|
"/.git/config", "//etc/passwd", "/app.js.map"):
|
||||||
|
r, _ = self.req("GET", path)
|
||||||
|
self.assertEqual(r.status, 404, path)
|
||||||
|
|
||||||
|
def test_round_trip_and_validation(self):
|
||||||
|
r, _ = self.req("GET", "/api/data")
|
||||||
|
self.assertEqual(r.status, 204)
|
||||||
|
good = blob()
|
||||||
|
r, _ = self.req("PUT", "/api/data", good)
|
||||||
|
self.assertEqual(r.status, 200)
|
||||||
|
r, data = self.req("GET", "/api/data")
|
||||||
|
self.assertEqual((r.status, data), (200, good))
|
||||||
|
self.assertEqual(os.stat(os.path.join(self.tmp.name, "ledger.enc")).st_mode & 0o777, 0o600)
|
||||||
|
bad = [b"not json", b"[]", b'{"salt":"a","iv":"b"}',
|
||||||
|
json.dumps({"salt": "!!", "iv": "AAAAAAAAAAAAAAAA", "ct": "AAAAAAAAAAAAAAAAAAAAAA=="}).encode(),
|
||||||
|
json.dumps({**json.loads(blob()), "extra": 1}).encode(),
|
||||||
|
json.dumps({**json.loads(blob()), "v": "1"}).encode(),
|
||||||
|
json.dumps({**json.loads(blob()), "salt": base64.b64encode(b"short").decode()}).encode(),
|
||||||
|
blob(ct_len=4)]
|
||||||
|
for body in bad:
|
||||||
|
r, _ = self.req("PUT", "/api/data", body)
|
||||||
|
self.assertEqual(r.status, 400, body)
|
||||||
|
r, _ = self.req("PUT", "/api/data", good, {"Content-Type": "text/plain"})
|
||||||
|
self.assertEqual(r.status, 415)
|
||||||
|
r, _ = self.req("PUT", "/api/data", good, {"Origin": "http://evil.example"})
|
||||||
|
self.assertEqual(r.status, 403)
|
||||||
|
r, _ = self.req("PUT", "/api/data", good, {"Sec-Fetch-Site": "cross-site"})
|
||||||
|
self.assertEqual(r.status, 403)
|
||||||
|
r, _ = self.req("PUT", "/api/data", good, {"Origin": f"http://127.0.0.1:{self.port}",
|
||||||
|
"Sec-Fetch-Site": "same-origin"})
|
||||||
|
self.assertEqual(r.status, 200)
|
||||||
|
r, _ = self.req("POST", "/api/data", good)
|
||||||
|
self.assertEqual(r.status, 405)
|
||||||
|
r, _ = self.req("PUT", "/index.html", good)
|
||||||
|
self.assertEqual(r.status, 405)
|
||||||
|
r, data = self.req("GET", "/api/data")
|
||||||
|
self.assertEqual(data, good) # none of the rejected bodies landed
|
||||||
|
|
||||||
|
def test_backups_rotate(self):
|
||||||
|
store = server.Handler.store
|
||||||
|
bodies = [blob() for _ in range(7)]
|
||||||
|
for b in bodies:
|
||||||
|
r, _ = self.req("PUT", "/api/data", b)
|
||||||
|
self.assertEqual(r.status, 200)
|
||||||
|
snaps = store._snapshots()
|
||||||
|
self.assertEqual(len(snaps), 3) # keep_recent=3, all taken today
|
||||||
|
with open(os.path.join(store.backups, snaps[-1]), "rb") as f:
|
||||||
|
self.assertEqual(f.read(), bodies[-2]) # the version before the last save
|
||||||
|
# Older days: the last snapshot of each of the 2 newest days survives.
|
||||||
|
for day in ("20260101", "20260102", "20260103"):
|
||||||
|
for t in ("T080000000000Z", "T200000000000Z"):
|
||||||
|
with open(os.path.join(store.backups, f"ledger-{day}{t}.enc"), "wb") as f:
|
||||||
|
f.write(b"old")
|
||||||
|
self.req("PUT", "/api/data", blob())
|
||||||
|
snaps = store._snapshots()
|
||||||
|
today = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d")
|
||||||
|
self.assertNotIn("ledger-20260101T200000000000Z.enc", snaps)
|
||||||
|
self.assertNotIn("ledger-20260102T200000000000Z.enc", snaps)
|
||||||
|
self.assertNotIn("ledger-20260103T080000000000Z.enc", snaps)
|
||||||
|
self.assertIn("ledger-20260103T200000000000Z.enc", snaps)
|
||||||
|
self.assertEqual(len([s for s in snaps if s[7:15] == today]), 3)
|
||||||
|
self.assertEqual(len(snaps), 4)
|
||||||
|
self.assertEqual([n for n in os.listdir(store.backups) if n.endswith(".tmp")], [])
|
||||||
|
|
||||||
|
def test_backup_interval(self):
|
||||||
|
server.Handler.store = store = server.Store(self.tmp.name, 3600, 10, 30)
|
||||||
|
for _ in range(4):
|
||||||
|
self.req("PUT", "/api/data", blob())
|
||||||
|
self.assertEqual(len(store._snapshots()), 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Apply the saved theme before paint so there's no flash. Dark by default.
|
||||||
|
// A separate file rather than an inline <script>: the server's CSP allows 'self' only.
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var t = localStorage.getItem("money-ledger-theme");
|
||||||
|
if (t !== "light" && t !== "dark") t = "dark";
|
||||||
|
document.documentElement.setAttribute("data-theme", t);
|
||||||
|
} catch (e) { document.documentElement.setAttribute("data-theme", "dark"); }
|
||||||
|
})();
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Vendored crypto
|
||||||
|
|
||||||
|
LibreLedger encrypts in the browser. Where the page is a secure context
|
||||||
|
(HTTPS, `http://localhost`) it uses the browser's Web Crypto API. Where it is
|
||||||
|
not (plain `http://` to a LAN or VPN address), browsers hide `crypto.subtle`,
|
||||||
|
and `crypto-fallback.js` runs the same PBKDF2-HMAC-SHA256 and AES-256-GCM from
|
||||||
|
these files instead. They are loaded only in that case, and the server serves
|
||||||
|
only the files listed in `server.py` (`VENDOR_FILES`).
|
||||||
|
|
||||||
|
| Package | Version | Released | Licence | npm tarball integrity |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| [@noble/hashes](https://github.com/paulmillr/noble-hashes) | 2.2.0 | 2026-04-11 | MIT | `sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==` |
|
||||||
|
| [@noble/ciphers](https://github.com/paulmillr/noble-ciphers) | 2.2.0 | 2026-04-11 | MIT | `sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==` |
|
||||||
|
|
||||||
|
Both are zero-dependency libraries by Paul Miller. Audit history, from their
|
||||||
|
READMEs: independent Cure53 audits of @noble/hashes 1.0.0 (Jan 2022) and
|
||||||
|
@noble/ciphers 1.0.0 (Sep 2024), and a self-audit of exactly 2.2.0 for both
|
||||||
|
(Apr 2026), which is why 2.2.0 is the version pinned here. The files below are **unmodified**
|
||||||
|
copies of the compiled ES modules from those tarballs, with each package's
|
||||||
|
`LICENSE`. Only the modules needed for PBKDF2-SHA256 and AES-GCM are kept.
|
||||||
|
The tarballs were checked against the integrity values above before
|
||||||
|
extracting.
|
||||||
|
|
||||||
|
## File hashes
|
||||||
|
|
||||||
|
`sha256sum`:
|
||||||
|
|
||||||
|
```
|
||||||
|
d079978bb7dac51e88586dd881ea3d7be010624b876d332f395f4a2b934f5fbc noble-ciphers-2.2.0/aes.js
|
||||||
|
f36671a5487c9c5050efacb58011c37c24c55a889803cb036cf9d9a6347c1e2d noble-ciphers-2.2.0/LICENSE
|
||||||
|
87a6cdf9cad2cec61229404cc7f8da1214952a76114b8a6342a3e771f1127cd1 noble-ciphers-2.2.0/_polyval.js
|
||||||
|
cfb9806b3339c79544a48a7026ee3c868cd1f0a1555c5eef4854b7eec43ccb63 noble-ciphers-2.2.0/utils.js
|
||||||
|
137ed94227806b351a55b09801287a4dba72d2a35d3838730becf641271bc3dd noble-hashes-2.2.0/hmac.js
|
||||||
|
4f221aee6e072336700c408c68ab3b96a3fc09f6aebe6f48f1bd99e5ef13faec noble-hashes-2.2.0/LICENSE
|
||||||
|
8227b9b5cabf078a9d7f7317f7a1ace6e46627539aa9364667aec724e1636f14 noble-hashes-2.2.0/_md.js
|
||||||
|
d61f870b99cf8e67b6df0ae5f582c74177445844d6a92ea51e8060032d05b555 noble-hashes-2.2.0/pbkdf2.js
|
||||||
|
0fb8e3c3f2c73a890be2524ac5d2542aaed4decff69e561231a86131203b3973 noble-hashes-2.2.0/sha2.js
|
||||||
|
766b91a693a798f9d3cde97b25db4a6d0cef66b2ca21153d3d42424d37878870 noble-hashes-2.2.0/_u64.js
|
||||||
|
e2adfc13c846487feff0410bd5508a1d66f5ebadc3188f3a40a6b55449981e2f noble-hashes-2.2.0/utils.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Subresource-integrity form (`sha384`):
|
||||||
|
|
||||||
|
```
|
||||||
|
sha384-0YHayko0KfpRNxkCNkZTuDkZIofvvDbgY4URpQALCIVKUglJYgnGQ4qGfXFFzXe8 noble-ciphers-2.2.0/aes.js
|
||||||
|
sha384-RCB3oUufLSfqmAfe/Csz1PRWhjGYjVj48y1X4s/XpyPQk1Bcd2Pa54pZCBo+u7O1 noble-ciphers-2.2.0/LICENSE
|
||||||
|
sha384-IHfhTh3OeAiHoY7cB8F9YUBJ9rn6RieetfuNQZY75cIgd5UcjVqOCh6ssn0gIGed noble-ciphers-2.2.0/_polyval.js
|
||||||
|
sha384-VZAiMxOScRfdOr1e2+N5h6GRH5I1mBeWGL1BSjmYLB9VuR/ODNHxvqbMzPIvwwXV noble-ciphers-2.2.0/utils.js
|
||||||
|
sha384-jjTbQ/7iXpfi7CA4vLI8VW2ljCxeMnfWE6FYpgMrpYgpT+IKKDJLplkP3KlaqLXp noble-hashes-2.2.0/hmac.js
|
||||||
|
sha384-a19tauEwIs1iP+2eFZxRsChHXNFBtRprDPX8zuYyn/tuG+DeZzAzH1Fv/gHlGrk7 noble-hashes-2.2.0/LICENSE
|
||||||
|
sha384-cQrKviUiqTLrOSU3x5KyKb6vIptltNRJQbJGyyXy0+QCUUpOHRwOLM0JuIE//YJB noble-hashes-2.2.0/_md.js
|
||||||
|
sha384-z3+NFrFNV0hs4V5jqy3EAGPvJQAi902QeBkbt5se8GRLG02Vbdz12785sBD3H/mx noble-hashes-2.2.0/pbkdf2.js
|
||||||
|
sha384-lVYA2l9kQw+NU1uh5QaTLMkTJaFpmFJuIohWWtXdZ5Ri+AdJ+gG7dWCnv0VpvBYt noble-hashes-2.2.0/sha2.js
|
||||||
|
sha384-j/S9oLRxzt1W++wUX4Lid0ZXyDVraPlN3I5Olt/q+FBuApzg+Tk6GAjTioMmzu0G noble-hashes-2.2.0/_u64.js
|
||||||
|
sha384-eVOwgoKAAt0Ur0m4yWkKHg9hlMn4HGGrUW0PlHqEzo7hYCelwnnIejIRYCoUIbbf noble-hashes-2.2.0/utils.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checking them yourself
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for p in hashes ciphers; do
|
||||||
|
curl -sSfO "https://registry.npmjs.org/@noble/$p/-/$p-2.2.0.tgz"
|
||||||
|
echo "$p sha512-$(openssl dgst -sha512 -binary $p-2.2.0.tgz | base64 -w0)" # compare with the table
|
||||||
|
mkdir -p "$p" && tar xzf "$p-2.2.0.tgz" -C "$p"
|
||||||
|
done
|
||||||
|
for f in noble-hashes-2.2.0/*; do cmp "$f" "hashes/package/${f#*/}"; done
|
||||||
|
for f in noble-ciphers-2.2.0/*; do cmp "$f" "ciphers/package/${f#*/}"; done
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
Pick a release that has been out for a while, verify its tarball integrity
|
||||||
|
against the npm registry, copy the same files into a new versioned folder,
|
||||||
|
update the imports in `crypto-fallback.js`, `VENDOR_FILES` in `server.py` and
|
||||||
|
this file, then run `tests/crypto-interop.test.mjs`. The modules still carry
|
||||||
|
their `//# sourceMappingURL` comments; the maps are not shipped, so a browser's
|
||||||
|
developer tools may note a missing map, which is harmless.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 Paul Miller (https://paulmillr.com)
|
||||||
|
Copyright (c) 2016 Thomas Pornin <pornin@bolet.org>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the “Software”), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
/**
|
||||||
|
* GHash from AES-GCM and its little-endian "mirror image" Polyval from AES-SIV.
|
||||||
|
*
|
||||||
|
* Implemented in terms of GHash with conversion function for keys
|
||||||
|
* GCM GHASH from
|
||||||
|
* {@link https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf | NIST SP800-38d},
|
||||||
|
* SIV from
|
||||||
|
* {@link https://www.rfc-editor.org/rfc/rfc8452 | RFC 8452}.
|
||||||
|
*
|
||||||
|
* GHASH modulo: x^128 + x^7 + x^2 + x + 1
|
||||||
|
* POLYVAL modulo: x^128 + x^127 + x^126 + x^121 + 1
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
import { abytes, aexists, aoutput, clean, copyBytes, createView, swap32IfBE, swap8IfBE, u32, wrapMacConstructor, } from "./utils.js";
|
||||||
|
const BLOCK_SIZE = 16;
|
||||||
|
// TODO: rewrite
|
||||||
|
// temporary padding buffer
|
||||||
|
// ZEROS32 aliases these bytes, so clean(ZEROS32) also resets this shared tail-padding scratch.
|
||||||
|
const ZEROS16 = /* @__PURE__ */ new Uint8Array(16);
|
||||||
|
const ZEROS32 = /* @__PURE__ */ u32(ZEROS16);
|
||||||
|
// GHASH reduces modulo x^128 + x^7 + x^2 + x + 1, so the low-degree terms
|
||||||
|
// x^7 + x^2 + x + 1 become bits `11100001` = 0xe1 in R = 0xe1 || 0^120.
|
||||||
|
const POLY = 0xe1;
|
||||||
|
// v = 2*v % POLY
|
||||||
|
// NOTE: because x + x = 0 (add/sub is same), mul2(x) != x+x
|
||||||
|
// Montgomery ladder can multiply any field element with this doubling step;
|
||||||
|
// addition stays simple xor.
|
||||||
|
const mul2 = (s0, s1, s2, s3) => {
|
||||||
|
const hiBit = s3 & 1;
|
||||||
|
return {
|
||||||
|
s3: (s2 << 31) | (s3 >>> 1),
|
||||||
|
s2: (s1 << 31) | (s2 >>> 1),
|
||||||
|
s1: (s0 << 31) | (s1 >>> 1),
|
||||||
|
// NIST SP 800-38D §6.3 applies `V >> 1` and XORs R on carry. In this
|
||||||
|
// 4x32-bit split, R = 0xe1 || 0^120 lives in the top byte of s0.
|
||||||
|
s0: (s0 >>> 1) ^ ((POLY << 24) & -(hiBit & 1)), // reduce % poly
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// Per-word part of RFC 8452 `ByteReverse`; callers also reverse the 32-bit word order.
|
||||||
|
const swapLE = (n) => (((n >>> 0) & 0xff) << 24) |
|
||||||
|
(((n >>> 8) & 0xff) << 16) |
|
||||||
|
(((n >>> 16) & 0xff) << 8) |
|
||||||
|
((n >>> 24) & 0xff) |
|
||||||
|
0;
|
||||||
|
// POLYVAL first applies RFC 8452's per-word byte reversal, then re-normalizes
|
||||||
|
// host-endian u32 loads to the little-endian word value `_updateBlock()` expects.
|
||||||
|
const swap8IfLE = (n) => swap8IfBE(swapLE(n));
|
||||||
|
/**
|
||||||
|
* `mulX_GHASH(ByteReverse(H))` from RFC 8452 Appendix A.
|
||||||
|
* @param k mutated in place
|
||||||
|
*/
|
||||||
|
export function _toGHASHKey(k) {
|
||||||
|
// The input is the original POLYVAL key H; reverse() materializes
|
||||||
|
// RFC 8452's `ByteReverse(H)` before the GHASH mulX step.
|
||||||
|
k.reverse();
|
||||||
|
const hiBit = k[15] & 1;
|
||||||
|
// k >>= 1
|
||||||
|
let carry = 0;
|
||||||
|
for (let i = 0; i < k.length; i++) {
|
||||||
|
const t = k[i];
|
||||||
|
k[i] = (t >>> 1) | carry;
|
||||||
|
carry = (t & 1) << 7;
|
||||||
|
}
|
||||||
|
k[0] ^= -hiBit & 0xe1; // if (hiBit) n ^= 0xe1000000000000000000000000000000;
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
// Precompute-window heuristic only: larger inputs trade memory for fewer table lookups.
|
||||||
|
// Any caller-provided length hint still collapses to one of the supported windows {2, 4, 8}.
|
||||||
|
const estimateWindow = (bytes) => {
|
||||||
|
if (bytes > 64 * 1024)
|
||||||
|
return 8;
|
||||||
|
if (bytes > 1024)
|
||||||
|
return 4;
|
||||||
|
return 2;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Incremental GHASH state for AES-GCM.
|
||||||
|
* @param key - 16-byte GHASH key.
|
||||||
|
* @param expectedLength - Expected message length for table sizing.
|
||||||
|
* Chunking is segment-based, not hash-streaming: every `update()` call is zero-padded
|
||||||
|
* to the next 16-byte boundary before it is absorbed. This matches the internal AES/GCM
|
||||||
|
* use where AAD, payload, and length block are separate padded segments.
|
||||||
|
* @example
|
||||||
|
* Feeds one ciphertext block into an incremental GHASH state with a fresh hash key.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import { GHASH } from '@noble/ciphers/_polyval.js';
|
||||||
|
* import { randomBytes } from '@noble/ciphers/utils.js';
|
||||||
|
* const key = randomBytes(16);
|
||||||
|
* const mac = new GHASH(key);
|
||||||
|
* mac.update(new Uint8Array(16));
|
||||||
|
* mac.digest();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class GHASH {
|
||||||
|
blockLen = BLOCK_SIZE;
|
||||||
|
outputLen = BLOCK_SIZE;
|
||||||
|
s0 = 0;
|
||||||
|
s1 = 0;
|
||||||
|
s2 = 0;
|
||||||
|
s3 = 0;
|
||||||
|
finished = false;
|
||||||
|
destroyed = false;
|
||||||
|
t;
|
||||||
|
W;
|
||||||
|
windowSize;
|
||||||
|
// We select bits per window adaptively based on expectedLength
|
||||||
|
constructor(key, expectedLength) {
|
||||||
|
abytes(key, 16, 'key');
|
||||||
|
key = copyBytes(key);
|
||||||
|
const kView = createView(key);
|
||||||
|
let k0 = kView.getUint32(0, false);
|
||||||
|
let k1 = kView.getUint32(4, false);
|
||||||
|
let k2 = kView.getUint32(8, false);
|
||||||
|
let k3 = kView.getUint32(12, false);
|
||||||
|
// generate table of doubled keys (half of montgomery ladder)
|
||||||
|
const doubles = [];
|
||||||
|
for (let i = 0; i < 128; i++) {
|
||||||
|
doubles.push({ s0: swapLE(k0), s1: swapLE(k1), s2: swapLE(k2), s3: swapLE(k3) });
|
||||||
|
({ s0: k0, s1: k1, s2: k2, s3: k3 } = mul2(k0, k1, k2, k3));
|
||||||
|
}
|
||||||
|
const W = estimateWindow(expectedLength || 1024);
|
||||||
|
if (![1, 2, 4, 8].includes(W))
|
||||||
|
throw new Error('ghash: invalid window size, expected 2, 4 or 8');
|
||||||
|
this.W = W;
|
||||||
|
const bits = 128; // always 128 bits;
|
||||||
|
const windows = bits / W;
|
||||||
|
const windowSize = (this.windowSize = 2 ** W);
|
||||||
|
const items = [];
|
||||||
|
// Create precompute table for window of W bits
|
||||||
|
for (let w = 0; w < windows; w++) {
|
||||||
|
// truth table: 00, 01, 10, 11
|
||||||
|
for (let byte = 0; byte < windowSize; byte++) {
|
||||||
|
// prettier-ignore
|
||||||
|
let s0 = 0, s1 = 0, s2 = 0, s3 = 0;
|
||||||
|
for (let j = 0; j < W; j++) {
|
||||||
|
const bit = (byte >>> (W - j - 1)) & 1;
|
||||||
|
if (!bit)
|
||||||
|
continue;
|
||||||
|
const { s0: d0, s1: d1, s2: d2, s3: d3 } = doubles[W * w + j];
|
||||||
|
((s0 ^= d0), (s1 ^= d1), (s2 ^= d2), (s3 ^= d3));
|
||||||
|
}
|
||||||
|
items.push({ s0, s1, s2, s3 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.t = items;
|
||||||
|
}
|
||||||
|
_updateBlock(s0, s1, s2, s3) {
|
||||||
|
((s0 ^= this.s0), (s1 ^= this.s1), (s2 ^= this.s2), (s3 ^= this.s3));
|
||||||
|
const { W, t, windowSize } = this;
|
||||||
|
// prettier-ignore
|
||||||
|
let o0 = 0, o1 = 0, o2 = 0, o3 = 0;
|
||||||
|
const mask = (1 << W) - 1; // 2**W will kill performance.
|
||||||
|
let w = 0;
|
||||||
|
// NIST SP 800-38D §6.3 interprets blocks as little-endian polynomials,
|
||||||
|
// so the lookup walk consumes each word byte-by-byte from
|
||||||
|
// least-significant to most-significant bits.
|
||||||
|
for (const num of [s0, s1, s2, s3]) {
|
||||||
|
for (let bytePos = 0; bytePos < 4; bytePos++) {
|
||||||
|
const byte = (num >>> (8 * bytePos)) & 0xff;
|
||||||
|
for (let bitPos = 8 / W - 1; bitPos >= 0; bitPos--) {
|
||||||
|
const bit = (byte >>> (W * bitPos)) & mask;
|
||||||
|
const { s0: e0, s1: e1, s2: e2, s3: e3 } = t[w * windowSize + bit];
|
||||||
|
((o0 ^= e0), (o1 ^= e1), (o2 ^= e2), (o3 ^= e3));
|
||||||
|
w += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.s0 = o0;
|
||||||
|
this.s1 = o1;
|
||||||
|
this.s2 = o2;
|
||||||
|
this.s3 = o3;
|
||||||
|
}
|
||||||
|
update(data) {
|
||||||
|
aexists(this);
|
||||||
|
abytes(data);
|
||||||
|
data = copyBytes(data);
|
||||||
|
const b32 = u32(data);
|
||||||
|
const blocks = Math.floor(data.length / BLOCK_SIZE);
|
||||||
|
const left = data.length % BLOCK_SIZE;
|
||||||
|
for (let i = 0; i < blocks; i++) {
|
||||||
|
this._updateBlock(swap8IfBE(b32[i * 4 + 0]), swap8IfBE(b32[i * 4 + 1]), swap8IfBE(b32[i * 4 + 2]), swap8IfBE(b32[i * 4 + 3]));
|
||||||
|
}
|
||||||
|
if (left) {
|
||||||
|
ZEROS16.set(data.subarray(blocks * BLOCK_SIZE));
|
||||||
|
// Tail blocks go through the shared ZEROS32 scratch, so they need the same host-endian
|
||||||
|
// normalization as full blocks; otherwise segmented GHASH/POLYVAL updates diverge on BE.
|
||||||
|
this._updateBlock(swap8IfBE(ZEROS32[0]), swap8IfBE(ZEROS32[1]), swap8IfBE(ZEROS32[2]), swap8IfBE(ZEROS32[3]));
|
||||||
|
clean(ZEROS32); // clean tmp buffer
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
// `aexists(this)` guards update/digest paths, so destroy must mark the instance unusable too.
|
||||||
|
this.destroyed = true;
|
||||||
|
const { t } = this;
|
||||||
|
// Wipe the key-derived precompute table; scalar accumulator words remain,
|
||||||
|
// but the destroyed guard blocks further use.
|
||||||
|
// clean precompute table
|
||||||
|
for (const elm of t) {
|
||||||
|
((elm.s0 = 0), (elm.s1 = 0), (elm.s2 = 0), (elm.s3 = 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
digestInto(out) {
|
||||||
|
aexists(this);
|
||||||
|
// `digestInto(out)` is the no-allocation fast path, so callers must pass a
|
||||||
|
// 32-bit-aligned buffer before we reinterpret it with `u32(out)`.
|
||||||
|
aoutput(out, this, true);
|
||||||
|
this.finished = true;
|
||||||
|
// NIST SP 800-38D §6.4 returns the final 128-bit block Y_m.
|
||||||
|
// `digestInto()` follows the relaxed `aoutput()` contract, so only
|
||||||
|
// out[0..15] may be touched.
|
||||||
|
const { s0, s1, s2, s3 } = this;
|
||||||
|
const o32 = u32(out);
|
||||||
|
o32[0] = s0;
|
||||||
|
o32[1] = s1;
|
||||||
|
o32[2] = s2;
|
||||||
|
o32[3] = s3;
|
||||||
|
swap32IfBE(o32);
|
||||||
|
}
|
||||||
|
digest() {
|
||||||
|
const res = new Uint8Array(BLOCK_SIZE);
|
||||||
|
this.digestInto(res);
|
||||||
|
// `res` is independent of internal state, so it stays valid after destroy() wipes the table.
|
||||||
|
this.destroy();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Incremental POLYVAL state for AES-SIV.
|
||||||
|
* @param key - 16-byte POLYVAL key.
|
||||||
|
* @param expectedLength - Expected message length for table sizing.
|
||||||
|
* Inherits GHASH's segment-padded `update()` behavior: each call is padded
|
||||||
|
* independently to a 16-byte boundary before absorption.
|
||||||
|
* @example
|
||||||
|
* Feeds one block into an incremental POLYVAL state with a fresh hash key.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import { Polyval } from '@noble/ciphers/_polyval.js';
|
||||||
|
* import { randomBytes } from '@noble/ciphers/utils.js';
|
||||||
|
* const key = randomBytes(16);
|
||||||
|
* const mac = new Polyval(key);
|
||||||
|
* mac.update(new Uint8Array(16));
|
||||||
|
* mac.digest();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class Polyval extends GHASH {
|
||||||
|
constructor(key, expectedLength) {
|
||||||
|
abytes(key);
|
||||||
|
// RFC 8452 Appendix A converts the POLYVAL key with
|
||||||
|
// `mulX_GHASH(ByteReverse(H))`; copy first because `_toGHASHKey(...)`
|
||||||
|
// mutates in place.
|
||||||
|
const ghKey = _toGHASHKey(copyBytes(key));
|
||||||
|
super(ghKey, expectedLength);
|
||||||
|
clean(ghKey);
|
||||||
|
}
|
||||||
|
update(data) {
|
||||||
|
aexists(this);
|
||||||
|
abytes(data);
|
||||||
|
data = copyBytes(data);
|
||||||
|
const b32 = u32(data);
|
||||||
|
const left = data.length % BLOCK_SIZE;
|
||||||
|
const blocks = Math.floor(data.length / BLOCK_SIZE);
|
||||||
|
for (let i = 0; i < blocks; i++) {
|
||||||
|
// RFC 8452 Appendix A feeds `ByteReverse(X_i)` into GHASH, so POLYVAL
|
||||||
|
// reverses the 32-bit word order in addition to the per-word byte swap.
|
||||||
|
this._updateBlock(swap8IfLE(b32[i * 4 + 3]), swap8IfLE(b32[i * 4 + 2]), swap8IfLE(b32[i * 4 + 1]), swap8IfLE(b32[i * 4 + 0]));
|
||||||
|
}
|
||||||
|
if (left) {
|
||||||
|
ZEROS16.set(data.subarray(blocks * BLOCK_SIZE));
|
||||||
|
this._updateBlock(swap8IfLE(ZEROS32[3]), swap8IfLE(ZEROS32[2]), swap8IfLE(ZEROS32[1]), swap8IfLE(ZEROS32[0]));
|
||||||
|
clean(ZEROS32);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
digestInto(out) {
|
||||||
|
aexists(this);
|
||||||
|
// `digestInto(out)` is the no-allocation fast path, so callers must pass a
|
||||||
|
// 32-bit-aligned buffer before we reinterpret the output prefix with `u32(view)`.
|
||||||
|
aoutput(out, this, true);
|
||||||
|
this.finished = true;
|
||||||
|
// RFC 8452 Appendix A maps POLYVAL output back through `ByteReverse(...)`.
|
||||||
|
// `digestInto()` follows the relaxed `aoutput()` contract, so only out[0..15] may be touched.
|
||||||
|
const view = out.subarray(0, this.outputLen);
|
||||||
|
const { s0, s1, s2, s3 } = this;
|
||||||
|
const o32 = u32(view);
|
||||||
|
o32[0] = s0;
|
||||||
|
o32[1] = s1;
|
||||||
|
o32[2] = s2;
|
||||||
|
o32[3] = s3;
|
||||||
|
swap32IfBE(o32);
|
||||||
|
view.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* GHash MAC for AES-GCM.
|
||||||
|
* @param msg - Message bytes to authenticate.
|
||||||
|
* @param key - 16-byte GHASH key.
|
||||||
|
* @returns 16-byte authentication tag.
|
||||||
|
* @example
|
||||||
|
* Authenticates a short message with GHASH and a fresh hash key.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import { ghash } from '@noble/ciphers/_polyval.js';
|
||||||
|
* import { randomBytes } from '@noble/ciphers/utils.js';
|
||||||
|
* const key = randomBytes(16);
|
||||||
|
* ghash(new Uint8Array(), key);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const ghash =
|
||||||
|
/* @__PURE__ */ wrapMacConstructor(16, (key, expectedLength) => new GHASH(key, expectedLength), (msg) => [msg.length]);
|
||||||
|
/**
|
||||||
|
* POLYVAL MAC for AES-SIV.
|
||||||
|
* @param msg - Message bytes to authenticate.
|
||||||
|
* @param key - 16-byte POLYVAL key.
|
||||||
|
* @returns 16-byte authentication tag.
|
||||||
|
* @example
|
||||||
|
* Authenticates a short message with POLYVAL and a fresh hash key.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import { polyval } from '@noble/ciphers/_polyval.js';
|
||||||
|
* import { randomBytes } from '@noble/ciphers/utils.js';
|
||||||
|
* const key = randomBytes(16);
|
||||||
|
* polyval(new Uint8Array(), key);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const polyval =
|
||||||
|
/* @__PURE__ */ wrapMacConstructor(16, (key, expectedLength) => new Polyval(key, expectedLength), (msg) => [msg.length]);
|
||||||
|
//# sourceMappingURL=_polyval.js.map
|
||||||
@@ -0,0 +1,807 @@
|
|||||||
|
/**
|
||||||
|
* Utilities for hex, bytes, CSPRNG.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
/*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */
|
||||||
|
/**
|
||||||
|
* Checks if something is Uint8Array. Be careful: nodejs Buffer will return true.
|
||||||
|
* @param a - Value to inspect.
|
||||||
|
* @returns `true` when the value is a Uint8Array view, including Node's `Buffer`.
|
||||||
|
* @example
|
||||||
|
* Guards a value before treating it as raw key material.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* isBytes(new Uint8Array());
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function isBytes(a) {
|
||||||
|
// Plain `instanceof Uint8Array` is too strict for some Buffer / proxy /
|
||||||
|
// cross-realm cases. The fallback still requires a real ArrayBuffer view
|
||||||
|
// so plain JSON-deserialized `{ constructor: ... }`
|
||||||
|
// spoofing is rejected, and `BYTES_PER_ELEMENT === 1` keeps the fallback on byte-oriented views.
|
||||||
|
return (a instanceof Uint8Array ||
|
||||||
|
(ArrayBuffer.isView(a) &&
|
||||||
|
a.constructor.name === 'Uint8Array' &&
|
||||||
|
'BYTES_PER_ELEMENT' in a &&
|
||||||
|
a.BYTES_PER_ELEMENT === 1));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Asserts something is boolean.
|
||||||
|
* @param b - Value to validate.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @example
|
||||||
|
* Validates a boolean option before branching on it.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* abool(true);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function abool(b) {
|
||||||
|
if (typeof b !== 'boolean')
|
||||||
|
throw new TypeError(`boolean expected, not ${b}`);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Asserts something is a non-negative safe integer.
|
||||||
|
* @param n - Value to validate.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @throws On wrong argument ranges or values. {@link RangeError}
|
||||||
|
* @example
|
||||||
|
* Validates a non-negative length or counter.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* anumber(1);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function anumber(n) {
|
||||||
|
if (typeof n !== 'number')
|
||||||
|
throw new TypeError('number expected, got ' + typeof n);
|
||||||
|
if (!Number.isSafeInteger(n) || n < 0)
|
||||||
|
throw new RangeError('positive integer expected, got ' + n);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Asserts something is Uint8Array.
|
||||||
|
* @param value - Value to validate.
|
||||||
|
* @param length - Expected byte length.
|
||||||
|
* @param title - Optional label used in error messages.
|
||||||
|
* @returns The validated byte array.
|
||||||
|
* On Node, `Buffer` is accepted too because it is a Uint8Array view.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @throws On wrong argument lengths. {@link RangeError}
|
||||||
|
* @example
|
||||||
|
* Validates a fixed-length nonce or key buffer.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* abytes(new Uint8Array([1, 2]), 2);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function abytes(value, length, title = '') {
|
||||||
|
const bytes = isBytes(value);
|
||||||
|
const len = value?.length;
|
||||||
|
const needsLen = length !== undefined;
|
||||||
|
if (!bytes || (needsLen && len !== length)) {
|
||||||
|
const prefix = title && `"${title}" `;
|
||||||
|
const ofLen = needsLen ? ` of length ${length}` : '';
|
||||||
|
const got = bytes ? `length=${len}` : `type=${typeof value}`;
|
||||||
|
const message = prefix + 'expected Uint8Array' + ofLen + ', got ' + got;
|
||||||
|
if (!bytes)
|
||||||
|
throw new TypeError(message);
|
||||||
|
throw new RangeError(message);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Asserts a hash- or MAC-like instance has not been destroyed or finished.
|
||||||
|
* @param instance - Stateful instance to validate.
|
||||||
|
* @param checkFinished - Whether to reject finished instances.
|
||||||
|
* When `false`, only `destroyed` is checked.
|
||||||
|
* @throws If the hash instance has already been destroyed or finalized. {@link Error}
|
||||||
|
* @example
|
||||||
|
* Guards against calling `update()` or `digest()` on a finished hash.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* aexists({ destroyed: false, finished: false });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function aexists(instance, checkFinished = true) {
|
||||||
|
if (instance.destroyed)
|
||||||
|
throw new Error('Hash instance has been destroyed');
|
||||||
|
if (checkFinished && instance.finished)
|
||||||
|
throw new Error('Hash#digest() has already been called');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Asserts output is a properly-sized byte array.
|
||||||
|
* @param out - Output buffer to validate.
|
||||||
|
* @param instance - Hash-like instance providing `outputLen`.
|
||||||
|
* This is the relaxed `digestInto()`-style contract: output must be at least `outputLen`,
|
||||||
|
* unlike one-shot cipher helpers elsewhere in the repo that often require exact lengths.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @param onlyAligned - Whether `out` must be 4-byte aligned for zero-allocation word views.
|
||||||
|
* @throws On wrong output buffer lengths. {@link RangeError}
|
||||||
|
* @throws On wrong output buffer alignment. {@link Error}
|
||||||
|
* @example
|
||||||
|
* Verifies that a caller-provided output buffer is large enough.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* aoutput(new Uint8Array(16), { outputLen: 16 });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function aoutput(out, instance, onlyAligned = false) {
|
||||||
|
abytes(out, undefined, 'output');
|
||||||
|
const min = instance.outputLen;
|
||||||
|
if (out.length < min) {
|
||||||
|
throw new RangeError('digestInto() expects output buffer of length at least ' + min);
|
||||||
|
}
|
||||||
|
if (onlyAligned && !isAligned32(out))
|
||||||
|
throw new Error('invalid output, must be aligned');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Casts a typed-array view to Uint8Array.
|
||||||
|
* @param arr - Typed-array view to reinterpret.
|
||||||
|
* @returns Uint8Array view over the same bytes.
|
||||||
|
* @example
|
||||||
|
* Views 32-bit words as raw bytes without copying.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* u8(new Uint32Array([1]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function u8(arr) {
|
||||||
|
return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Casts a typed-array view to Uint32Array.
|
||||||
|
* @param arr - Typed-array view to reinterpret.
|
||||||
|
* @returns Uint32Array view over the same bytes. Callers are expected to provide a
|
||||||
|
* 4-byte-aligned offset; trailing `1..3` bytes are silently dropped.
|
||||||
|
* @example
|
||||||
|
* Views a byte buffer as 32-bit words for block processing.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* u32(new Uint8Array(4));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function u32(arr) {
|
||||||
|
return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Zeroizes typed arrays in place.
|
||||||
|
* Warning: JS provides no guarantees.
|
||||||
|
* @param arrays - Arrays to wipe.
|
||||||
|
* @example
|
||||||
|
* Wipes a temporary key buffer after use.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const bytes = new Uint8Array([1]);
|
||||||
|
* clean(bytes);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function clean(...arrays) {
|
||||||
|
for (let i = 0; i < arrays.length; i++) {
|
||||||
|
arrays[i].fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a DataView for byte-level manipulation.
|
||||||
|
* @param arr - Typed-array view to wrap.
|
||||||
|
* @returns DataView over the same bytes.
|
||||||
|
* @example
|
||||||
|
* Creates an endian-aware view for length encoding.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* createView(new Uint8Array(4));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createView(arr) {
|
||||||
|
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Whether the current platform is little-endian.
|
||||||
|
* Most are; some IBM systems are not.
|
||||||
|
*/
|
||||||
|
export const isLE = /* @__PURE__ */ (() => new Uint8Array(new Uint32Array([0x11223344]).buffer)[0] === 0x44)();
|
||||||
|
/**
|
||||||
|
* Reverses byte order of one 32-bit word.
|
||||||
|
* @param word - Unsigned 32-bit word to swap.
|
||||||
|
* @returns The same word with bytes reversed.
|
||||||
|
* @example
|
||||||
|
* Swaps a big-endian word into little-endian byte order.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* byteSwap(0x11223344);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const byteSwap = (word) => ((word << 24) & 0xff000000) |
|
||||||
|
((word << 8) & 0xff0000) |
|
||||||
|
((word >>> 8) & 0xff00) |
|
||||||
|
((word >>> 24) & 0xff);
|
||||||
|
/**
|
||||||
|
* Normalizes one 32-bit word to the little-endian representation expected by cipher cores.
|
||||||
|
* @param n - Unsigned 32-bit word to normalize.
|
||||||
|
* @returns Little-endian normalized word on big-endian hosts, else the input word unchanged.
|
||||||
|
* @example
|
||||||
|
* Normalizes a host-endian word before passing it into an ARX/AES core.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* swap8IfBE(0x11223344);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const swap8IfBE = isLE
|
||||||
|
? (n) => n
|
||||||
|
: (n) => byteSwap(n) >>> 0;
|
||||||
|
/**
|
||||||
|
* Byte-swaps every word of a Uint32Array in place.
|
||||||
|
* @param arr - Uint32Array whose words should be swapped.
|
||||||
|
* @returns The same array after in-place byte swapping.
|
||||||
|
* @example
|
||||||
|
* Swaps every 32-bit word in a word-view buffer.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* byteSwap32(new Uint32Array([0x11223344]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const byteSwap32 = (arr) => {
|
||||||
|
for (let i = 0; i < arr.length; i++)
|
||||||
|
arr[i] = byteSwap(arr[i]);
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Normalizes a Uint32Array view to the little-endian representation expected by cipher cores.
|
||||||
|
* @param u - Word view to normalize in place.
|
||||||
|
* @returns Little-endian normalized word view.
|
||||||
|
* @example
|
||||||
|
* Normalizes a word-view buffer before block processing.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* swap32IfBE(new Uint32Array([0x11223344]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const swap32IfBE = isLE
|
||||||
|
? (u) => u
|
||||||
|
: byteSwap32;
|
||||||
|
// Built-in hex conversion:
|
||||||
|
// {@link https://caniuse.com/mdn-javascript_builtins_uint8array_fromhex | caniuse entry}
|
||||||
|
const hasHexBuiltin = /* @__PURE__ */ (() =>
|
||||||
|
// @ts-ignore
|
||||||
|
typeof Uint8Array.from([]).toHex === 'function' && typeof Uint8Array.fromHex === 'function')();
|
||||||
|
// Array where index 0xf0 (240) is mapped to string 'f0'
|
||||||
|
const hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'));
|
||||||
|
/**
|
||||||
|
* Convert byte array to hex string. Uses built-in function, when available.
|
||||||
|
* @param bytes - Bytes to encode.
|
||||||
|
* @returns Lowercase hexadecimal string.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @example
|
||||||
|
* Formats ciphertext bytes for logs or test vectors.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* bytesToHex(Uint8Array.from([0xca, 0xfe, 0x01, 0x23])); // 'cafe0123'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function bytesToHex(bytes) {
|
||||||
|
abytes(bytes);
|
||||||
|
// @ts-ignore
|
||||||
|
if (hasHexBuiltin)
|
||||||
|
return bytes.toHex();
|
||||||
|
// pre-caching improves the speed 6x
|
||||||
|
let hex = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
hex += hexes[bytes[i]];
|
||||||
|
}
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
// We use optimized technique to convert hex string to byte array
|
||||||
|
const asciis = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 };
|
||||||
|
function asciiToBase16(ch) {
|
||||||
|
if (ch >= asciis._0 && ch <= asciis._9)
|
||||||
|
return ch - asciis._0; // '2' => 50-48
|
||||||
|
if (ch >= asciis.A && ch <= asciis.F)
|
||||||
|
return ch - (asciis.A - 10); // 'B' => 66-(65-10)
|
||||||
|
if (ch >= asciis.a && ch <= asciis.f)
|
||||||
|
return ch - (asciis.a - 10); // 'b' => 98-(97-10)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Convert hex string to byte array. Uses built-in function, when available.
|
||||||
|
* @param hex - Hexadecimal string to decode.
|
||||||
|
* @returns Decoded bytes.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @throws On malformed hexadecimal input. {@link RangeError}
|
||||||
|
* @example
|
||||||
|
* Parses a hex test vector into bytes.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* hexToBytes('cafe0123'); // Uint8Array.from([0xca, 0xfe, 0x01, 0x23])
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function hexToBytes(hex) {
|
||||||
|
if (typeof hex !== 'string')
|
||||||
|
throw new TypeError('hex string expected, got ' + typeof hex);
|
||||||
|
if (hasHexBuiltin) {
|
||||||
|
try {
|
||||||
|
return Uint8Array.fromHex(hex);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof SyntaxError)
|
||||||
|
throw new RangeError(error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hl = hex.length;
|
||||||
|
const al = hl / 2;
|
||||||
|
if (hl % 2)
|
||||||
|
throw new RangeError('hex string expected, got unpadded hex of length ' + hl);
|
||||||
|
const array = new Uint8Array(al);
|
||||||
|
for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {
|
||||||
|
const n1 = asciiToBase16(hex.charCodeAt(hi));
|
||||||
|
const n2 = asciiToBase16(hex.charCodeAt(hi + 1));
|
||||||
|
if (n1 === undefined || n2 === undefined) {
|
||||||
|
const char = hex[hi] + hex[hi + 1];
|
||||||
|
throw new RangeError('hex string expected, got non-hex character "' + char + '" at index ' + hi);
|
||||||
|
}
|
||||||
|
array[ai] = n1 * 16 + n2; // multiply first octet, e.g. 'a3' => 10*16+3 => 160 + 3 => 163
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
// Used in micro
|
||||||
|
/**
|
||||||
|
* Converts a big-endian hex string into bigint.
|
||||||
|
* @param hex - Hexadecimal string without `0x`.
|
||||||
|
* @returns Parsed bigint value. The empty string is treated as `0n`.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @example
|
||||||
|
* Parses a big-endian field element or counter from hex.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* hexToNumber('ff');
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function hexToNumber(hex) {
|
||||||
|
if (typeof hex !== 'string')
|
||||||
|
throw new TypeError('hex string expected, got ' + typeof hex);
|
||||||
|
return BigInt(hex === '' ? '0' : '0x' + hex); // Big Endian
|
||||||
|
}
|
||||||
|
// Used in ff1
|
||||||
|
// BE: Big Endian, LE: Little Endian
|
||||||
|
/**
|
||||||
|
* Converts big-endian bytes into bigint.
|
||||||
|
* @param bytes - Big-endian bytes.
|
||||||
|
* @returns Parsed bigint value. Empty input is treated as `0n`.
|
||||||
|
* @throws On invalid byte input passed to the internal hex conversion. {@link TypeError}
|
||||||
|
* @example
|
||||||
|
* Reads a big-endian integer from serialized bytes.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* bytesToNumberBE(new Uint8Array([1, 0]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function bytesToNumberBE(bytes) {
|
||||||
|
return hexToNumber(bytesToHex(bytes));
|
||||||
|
}
|
||||||
|
// Used in micro, ff1
|
||||||
|
/**
|
||||||
|
* Converts a number into big-endian bytes of fixed length.
|
||||||
|
* @param n - Number to encode.
|
||||||
|
* @param len - Output length in bytes.
|
||||||
|
* @returns Big-endian bytes padded to `len`.
|
||||||
|
* Validation is indirect through `hexToBytes(...)`, so negative values, `len = 0`,
|
||||||
|
* and values that do not fit surface through the downstream hex parser instead of a
|
||||||
|
* dedicated range guard here.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @throws If the requested output length cannot represent the encoded value. {@link RangeError}
|
||||||
|
* @example
|
||||||
|
* Encodes a counter as fixed-width big-endian bytes.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* numberToBytesBE(1, 2);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function numberToBytesBE(n, len) {
|
||||||
|
// Reject coercible non-numeric inputs before string/hex conversion changes behavior.
|
||||||
|
if (typeof n === 'number')
|
||||||
|
anumber(n);
|
||||||
|
else if (typeof n !== 'bigint')
|
||||||
|
throw new TypeError(`number or bigint expected, got ${typeof n}`);
|
||||||
|
anumber(len);
|
||||||
|
return hexToBytes(n.toString(16).padStart(len * 2, '0'));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Converts string to bytes using UTF8 encoding.
|
||||||
|
* @param str - String to encode.
|
||||||
|
* @returns UTF-8 bytes in a detached fresh Uint8Array copy.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @example
|
||||||
|
* Encodes application text before encryption or MACing.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* utf8ToBytes('abc'); // new Uint8Array([97, 98, 99])
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function utf8ToBytes(str) {
|
||||||
|
if (typeof str !== 'string')
|
||||||
|
throw new TypeError('string expected');
|
||||||
|
return new Uint8Array(new TextEncoder().encode(str)); // {@link https://bugzil.la/1681809 | Firefox bug 1681809}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Converts bytes to string using UTF8 encoding.
|
||||||
|
* @param bytes - UTF-8 bytes.
|
||||||
|
* @returns Decoded string. Input validation is delegated to `TextDecoder`, and malformed
|
||||||
|
* UTF-8 is replacement-decoded instead of rejected.
|
||||||
|
* @example
|
||||||
|
* Decodes UTF-8 plaintext back into a string.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* bytesToUtf8(new Uint8Array([97, 98, 99])); // 'abc'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function bytesToUtf8(bytes) {
|
||||||
|
return new TextDecoder().decode(bytes);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks if two U8A use same underlying buffer and overlaps.
|
||||||
|
* This is invalid and can corrupt data.
|
||||||
|
* @param a - First byte view.
|
||||||
|
* @param b - Second byte view.
|
||||||
|
* @returns `true` when the views overlap in memory.
|
||||||
|
* @example
|
||||||
|
* Detects whether two slices alias the same backing buffer.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* overlapBytes(new Uint8Array(4), new Uint8Array(4));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function overlapBytes(a, b) {
|
||||||
|
// Zero-length views cannot overwrite anything, even if their offset sits inside another range.
|
||||||
|
if (!a.byteLength || !b.byteLength)
|
||||||
|
return false;
|
||||||
|
return (a.buffer === b.buffer && // best we can do, may fail with an obscure Proxy
|
||||||
|
a.byteOffset < b.byteOffset + b.byteLength && // a starts before b end
|
||||||
|
b.byteOffset < a.byteOffset + a.byteLength // b starts before a end
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* If input and output overlap and input starts before output, we will overwrite end of input before
|
||||||
|
* we start processing it, so this is not supported for most ciphers
|
||||||
|
* (except chacha/salsa, which were designed for this)
|
||||||
|
* @param input - Input bytes.
|
||||||
|
* @param output - Output bytes.
|
||||||
|
* @throws If the output view would overwrite unread input bytes. {@link Error}
|
||||||
|
* @example
|
||||||
|
* Rejects an in-place layout that would overwrite unread input bytes.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* complexOverlapBytes(new Uint8Array(4), new Uint8Array(4));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function complexOverlapBytes(input, output) {
|
||||||
|
// This is very cursed. It works somehow, but I'm completely unsure,
|
||||||
|
// reasoning about overlapping aligned windows is very hard.
|
||||||
|
if (overlapBytes(input, output) && input.byteOffset < output.byteOffset)
|
||||||
|
throw new Error('complex overlap of input and output is not supported');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Copies several Uint8Arrays into one.
|
||||||
|
* @param arrays - Byte arrays to concatenate.
|
||||||
|
* @returns Combined byte array.
|
||||||
|
* @throws On wrong argument types inside the byte-array list. {@link TypeError}
|
||||||
|
* @example
|
||||||
|
* Builds a `nonce || ciphertext` style buffer.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* concatBytes(new Uint8Array([1]), new Uint8Array([2]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function concatBytes(...arrays) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < arrays.length; i++) {
|
||||||
|
const a = arrays[i];
|
||||||
|
abytes(a);
|
||||||
|
sum += a.length;
|
||||||
|
}
|
||||||
|
const res = new Uint8Array(sum);
|
||||||
|
for (let i = 0, pad = 0; i < arrays.length; i++) {
|
||||||
|
const a = arrays[i];
|
||||||
|
res.set(a, pad);
|
||||||
|
pad += a.length;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Merges user options into defaults.
|
||||||
|
* @param defaults - Default option values.
|
||||||
|
* @param opts - User-provided overrides.
|
||||||
|
* @returns Combined options object.
|
||||||
|
* The merge mutates `defaults` in place and returns the same object.
|
||||||
|
* @throws If options are missing or not an object. {@link Error}
|
||||||
|
* @example
|
||||||
|
* Applies user overrides to the default cipher options.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* checkOpts({ rounds: 20 }, { rounds: 8 });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function checkOpts(defaults, opts) {
|
||||||
|
if (opts == null || typeof opts !== 'object')
|
||||||
|
throw new Error('options must be defined');
|
||||||
|
const merged = Object.assign(defaults, opts);
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Compares two byte arrays in kinda constant time once lengths already match.
|
||||||
|
* @param a - First byte array.
|
||||||
|
* @param b - Second byte array.
|
||||||
|
* @returns `true` when the arrays contain the same bytes. Different lengths still return early.
|
||||||
|
* @example
|
||||||
|
* Compares an expected authentication tag with the received one.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* equalBytes(new Uint8Array([1]), new Uint8Array([1]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function equalBytes(a, b) {
|
||||||
|
if (a.length !== b.length)
|
||||||
|
return false;
|
||||||
|
let diff = 0;
|
||||||
|
for (let i = 0; i < a.length; i++)
|
||||||
|
diff |= a[i] ^ b[i];
|
||||||
|
return diff === 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wraps a keyed MAC constructor into a one-shot helper with `.create()`.
|
||||||
|
* @param keyLen - Valid probe-key length used to read static metadata once.
|
||||||
|
* The probe key is only used for `outputLen` / `blockLen`, so callers with several valid key sizes
|
||||||
|
* can pass any representative size as long as those values stay fixed.
|
||||||
|
* @param macCons - Keyed MAC constructor or factory.
|
||||||
|
* @param fromMsg - Optional adapter that derives extra constructor args from the one-shot message.
|
||||||
|
* @returns Callable MAC helper with `.create()`.
|
||||||
|
*/
|
||||||
|
export function wrapMacConstructor(keyLen, macCons, fromMsg) {
|
||||||
|
const mac = macCons;
|
||||||
|
const getArgs = (fromMsg || (() => []));
|
||||||
|
const macC = (msg, key) => mac(key, ...getArgs(msg))
|
||||||
|
.update(msg)
|
||||||
|
.digest();
|
||||||
|
const tmp = mac(new Uint8Array(keyLen), ...getArgs(new Uint8Array(0)));
|
||||||
|
macC.outputLen = tmp.outputLen;
|
||||||
|
macC.blockLen = tmp.blockLen;
|
||||||
|
macC.create = (key, ...args) => mac(key, ...args);
|
||||||
|
return macC;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wraps a cipher: validates args, ensures encrypt() can only be called once.
|
||||||
|
* Used internally by the exported cipher constructors.
|
||||||
|
* Output-buffer support is inferred from the wrapped `encrypt` / `decrypt`
|
||||||
|
* arity (`fn.length === 2`), and tag-bearing constructors are expected to use
|
||||||
|
* `args[1]` for optional AAD.
|
||||||
|
* @__NO_SIDE_EFFECTS__
|
||||||
|
* @param params - Static cipher metadata. See {@link CipherParams}.
|
||||||
|
* @param constructor - Cipher constructor.
|
||||||
|
* @returns Wrapped constructor with validation.
|
||||||
|
*/
|
||||||
|
export const wrapCipher = (params, constructor) => {
|
||||||
|
function wrappedCipher(key, ...args) {
|
||||||
|
// Validate key
|
||||||
|
abytes(key, undefined, 'key');
|
||||||
|
// Validate nonce if nonceLength is present
|
||||||
|
if (params.nonceLength !== undefined) {
|
||||||
|
const nonce = args[0];
|
||||||
|
abytes(nonce, params.varSizeNonce ? undefined : params.nonceLength, 'nonce');
|
||||||
|
}
|
||||||
|
// Validate AAD if tagLength present
|
||||||
|
const tagl = params.tagLength;
|
||||||
|
if (tagl && args[1] !== undefined)
|
||||||
|
abytes(args[1], undefined, 'AAD');
|
||||||
|
const cipher = constructor(key, ...args);
|
||||||
|
const checkOutput = (fnLength, output) => {
|
||||||
|
if (output !== undefined) {
|
||||||
|
if (fnLength !== 2)
|
||||||
|
throw new Error('cipher output not supported');
|
||||||
|
abytes(output, undefined, 'output');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Create wrapped cipher with validation and single-use encryption
|
||||||
|
let called = false;
|
||||||
|
const wrCipher = {
|
||||||
|
encrypt(data, output) {
|
||||||
|
if (called)
|
||||||
|
throw new Error('cannot encrypt() twice with same key + nonce');
|
||||||
|
called = true;
|
||||||
|
abytes(data);
|
||||||
|
checkOutput(cipher.encrypt.length, output);
|
||||||
|
return cipher.encrypt(data, output);
|
||||||
|
},
|
||||||
|
decrypt(data, output) {
|
||||||
|
abytes(data);
|
||||||
|
if (tagl && data.length < tagl)
|
||||||
|
throw new Error('"ciphertext" expected length bigger than tagLength=' + tagl);
|
||||||
|
checkOutput(cipher.decrypt.length, output);
|
||||||
|
return cipher.decrypt(data, output);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return wrCipher;
|
||||||
|
}
|
||||||
|
Object.assign(wrappedCipher, params);
|
||||||
|
return wrappedCipher;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* By default, returns u8a of length.
|
||||||
|
* When out is available, it checks it for validity and uses it.
|
||||||
|
* @param expectedLength - Required output length.
|
||||||
|
* @param out - Optional destination buffer.
|
||||||
|
* @param onlyAligned - Whether `out` must be 4-byte aligned.
|
||||||
|
* @returns Output buffer ready for writing.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @throws If the provided output buffer has the wrong size or alignment. {@link Error}
|
||||||
|
* @example
|
||||||
|
* Reuses a caller-provided output buffer when lengths match.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* getOutput(16, new Uint8Array(16));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function getOutput(expectedLength, out, onlyAligned = true) {
|
||||||
|
if (out === undefined)
|
||||||
|
return new Uint8Array(expectedLength);
|
||||||
|
// Keep Buffer/cross-realm Uint8Array support here instead of trusting a shape-compatible object.
|
||||||
|
abytes(out, undefined, 'output');
|
||||||
|
if (out.length !== expectedLength)
|
||||||
|
throw new Error('"output" expected Uint8Array of length ' + expectedLength + ', got: ' + out.length);
|
||||||
|
if (onlyAligned && !isAligned32(out))
|
||||||
|
throw new Error('invalid output, must be aligned');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encodes data and AAD bit lengths into a 16-byte buffer.
|
||||||
|
* @param dataLength - Data length in bits.
|
||||||
|
* @param aadLength - AAD length in bits.
|
||||||
|
* The serialized block is still `aadLength || dataLength`, matching GCM/Poly1305
|
||||||
|
* conventions even though the helper parameter order is `(dataLength, aadLength)`.
|
||||||
|
* @param isLE - Whether to encode lengths as little-endian.
|
||||||
|
* @returns 16-byte length block.
|
||||||
|
* @throws On wrong argument types passed to the endian validator. {@link TypeError}
|
||||||
|
* @throws On wrong argument ranges or values. {@link RangeError}
|
||||||
|
* @example
|
||||||
|
* Builds the length block appended by GCM and Poly1305.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* u64Lengths(16, 8, true);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function u64Lengths(dataLength, aadLength, isLE) {
|
||||||
|
// Reject coercible non-number lengths like '10' and true before BigInt(...) accepts them.
|
||||||
|
anumber(dataLength);
|
||||||
|
anumber(aadLength);
|
||||||
|
abool(isLE);
|
||||||
|
const num = new Uint8Array(16);
|
||||||
|
const view = createView(num);
|
||||||
|
view.setBigUint64(0, BigInt(aadLength), isLE);
|
||||||
|
view.setBigUint64(8, BigInt(dataLength), isLE);
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks whether a byte array is aligned to a 4-byte offset.
|
||||||
|
* @param bytes - Byte array to inspect.
|
||||||
|
* @returns `true` when the view is 4-byte aligned.
|
||||||
|
* @example
|
||||||
|
* Checks whether a buffer can be safely viewed as Uint32Array.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* isAligned32(new Uint8Array(4));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function isAligned32(bytes) {
|
||||||
|
return bytes.byteOffset % 4 === 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Copies bytes into a new Uint8Array.
|
||||||
|
* @param bytes - Bytes to copy.
|
||||||
|
* @returns Copied byte array.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @example
|
||||||
|
* Copies input into an aligned Uint8Array before block processing.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* copyBytes(new Uint8Array([1, 2]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function copyBytes(bytes) {
|
||||||
|
// `Uint8Array.from(...)` would also accept arrays / other typed arrays. Keep this helper strict
|
||||||
|
// because callers use it at byte-validation boundaries before mutating the detached copy.
|
||||||
|
return Uint8Array.from(abytes(bytes));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Cryptographically secure PRNG.
|
||||||
|
* Uses internal OS-level `crypto.getRandomValues`.
|
||||||
|
* @param bytesLength - Number of bytes to produce.
|
||||||
|
* Validation is delegated to `Uint8Array(bytesLength)` and `getRandomValues`, so
|
||||||
|
* non-integers, negative lengths, and oversize requests surface backend/runtime errors.
|
||||||
|
* @returns Random byte array.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @throws On wrong argument ranges or values. {@link RangeError}
|
||||||
|
* @throws If the runtime does not expose `crypto.getRandomValues`. {@link Error}
|
||||||
|
* @example
|
||||||
|
* Generates a fresh nonce or key.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* randomBytes(16);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function randomBytes(bytesLength = 32) {
|
||||||
|
// Validate upfront so fractional / coercible lengths do not silently
|
||||||
|
// truncate through Uint8Array().
|
||||||
|
anumber(bytesLength);
|
||||||
|
const cr = typeof globalThis === 'object' ? globalThis.crypto : null;
|
||||||
|
if (typeof cr?.getRandomValues !== 'function')
|
||||||
|
throw new Error('crypto.getRandomValues must be defined');
|
||||||
|
return cr.getRandomValues(new Uint8Array(bytesLength));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Uses CSPRNG for nonce, nonce injected in ciphertext.
|
||||||
|
* For `encrypt`, a `nonceBytes`-length buffer is fetched from CSPRNG and
|
||||||
|
* prepended to encrypted ciphertext. For `decrypt`, first `nonceBytes` of ciphertext
|
||||||
|
* are treated as nonce. The wrapper always allocates a fresh `nonce || ciphertext`
|
||||||
|
* buffer on encrypt and intentionally does not support caller-provided destination buffers.
|
||||||
|
* Too-short decrypt inputs are split into short/empty nonce views and then delegated
|
||||||
|
* to the wrapped cipher instead of being rejected here first.
|
||||||
|
*
|
||||||
|
* NOTE: Under the same key, using random nonces (e.g. `managedNonce`) with AES-GCM and ChaCha
|
||||||
|
* should be limited to `2**23` (8M) messages to get a collision chance of
|
||||||
|
* `2**-50`. Stretching to `2**32` (4B) messages would raise that chance to
|
||||||
|
* `2**-33`, still negligible but creeping up.
|
||||||
|
* @param fn - Cipher constructor that expects a nonce.
|
||||||
|
* @param randomBytes_ - Random-byte source used for nonce generation.
|
||||||
|
* @returns Cipher constructor that prepends the nonce to ciphertext.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @throws On invalid nonce lengths observed at wrapper construction or use. {@link RangeError}
|
||||||
|
* @example
|
||||||
|
* Prepends a fresh random nonce to every ciphertext.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import { gcm } from '@noble/ciphers/aes.js';
|
||||||
|
* import { managedNonce, randomBytes } from '@noble/ciphers/utils.js';
|
||||||
|
* const wrapped = managedNonce(gcm);
|
||||||
|
* const key = randomBytes(16);
|
||||||
|
* const ciphertext = wrapped(key).encrypt(new Uint8Array([1, 2, 3]));
|
||||||
|
* wrapped(key).decrypt(ciphertext);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function managedNonce(fn, randomBytes_ = randomBytes) {
|
||||||
|
const { nonceLength } = fn;
|
||||||
|
anumber(nonceLength);
|
||||||
|
const addNonce = (nonce, ciphertext, plaintext) => {
|
||||||
|
const out = concatBytes(nonce, ciphertext);
|
||||||
|
// Wrapped ciphers may alias caller plaintext on encrypt(); never zero
|
||||||
|
// caller-owned buffers here.
|
||||||
|
if (!overlapBytes(plaintext, ciphertext))
|
||||||
|
ciphertext.fill(0);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
// NOTE: we cannot support DST here, it would be mistake:
|
||||||
|
// - we don't know how much dst length cipher requires
|
||||||
|
// - nonce may unalign dst and break everything
|
||||||
|
// - we create new u8a anyway (concatBytes)
|
||||||
|
// - previously we passed all args to cipher, but that was mistake!
|
||||||
|
const res = ((key, ...args) => ({
|
||||||
|
encrypt(plaintext) {
|
||||||
|
abytes(plaintext);
|
||||||
|
const nonce = randomBytes_(nonceLength);
|
||||||
|
const encrypted = fn(key, nonce, ...args).encrypt(plaintext);
|
||||||
|
// @ts-ignore
|
||||||
|
if (encrypted instanceof Promise)
|
||||||
|
return encrypted.then((ct) => addNonce(nonce, ct, plaintext));
|
||||||
|
return addNonce(nonce, encrypted, plaintext);
|
||||||
|
},
|
||||||
|
decrypt(ciphertext) {
|
||||||
|
abytes(ciphertext);
|
||||||
|
const nonce = ciphertext.subarray(0, nonceLength);
|
||||||
|
const decrypted = ciphertext.subarray(nonceLength);
|
||||||
|
return fn(key, nonce, ...args).decrypt(decrypted);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
// Auto-nonce wrappers still preserve the wrapped payload geometry.
|
||||||
|
if ('blockSize' in fn)
|
||||||
|
res.blockSize = fn.blockSize;
|
||||||
|
if ('tagLength' in fn)
|
||||||
|
res.tagLength = fn.tagLength;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=utils.js.map
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 Paul Miller (https://paulmillr.com)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the “Software”), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/**
|
||||||
|
* Internal Merkle-Damgard hash utils.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
import { abytes, aexists, aoutput, clean, createView, } from "./utils.js";
|
||||||
|
/**
|
||||||
|
* Shared 32-bit conditional boolean primitive reused by SHA-256, SHA-1, and MD5 `F`.
|
||||||
|
* Returns bits from `b` when `a` is set, otherwise from `c`.
|
||||||
|
* The XOR form is equivalent to MD5's `F(X,Y,Z) = XY v not(X)Z` because the masked terms never
|
||||||
|
* set the same bit.
|
||||||
|
* @param a - selector word
|
||||||
|
* @param b - word chosen when selector bit is set
|
||||||
|
* @param c - word chosen when selector bit is clear
|
||||||
|
* @returns Mixed 32-bit word.
|
||||||
|
* @example
|
||||||
|
* Combine three words with the shared 32-bit choice primitive.
|
||||||
|
* ```ts
|
||||||
|
* Chi(0xffffffff, 0x12345678, 0x87654321);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function Chi(a, b, c) {
|
||||||
|
return (a & b) ^ (~a & c);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Shared 32-bit majority primitive reused by SHA-256 and SHA-1.
|
||||||
|
* Returns bits shared by at least two inputs.
|
||||||
|
* @param a - first input word
|
||||||
|
* @param b - second input word
|
||||||
|
* @param c - third input word
|
||||||
|
* @returns Mixed 32-bit word.
|
||||||
|
* @example
|
||||||
|
* Combine three words with the shared 32-bit majority primitive.
|
||||||
|
* ```ts
|
||||||
|
* Maj(0xffffffff, 0x12345678, 0x87654321);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function Maj(a, b, c) {
|
||||||
|
return (a & b) ^ (a & c) ^ (b & c);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Merkle-Damgard hash construction base class.
|
||||||
|
* Could be used to create MD5, RIPEMD, SHA1, SHA2.
|
||||||
|
* Accepts only byte-aligned `Uint8Array` input, even when the underlying spec describes bit
|
||||||
|
* strings with partial-byte tails.
|
||||||
|
* @param blockLen - internal block size in bytes
|
||||||
|
* @param outputLen - digest size in bytes
|
||||||
|
* @param padOffset - trailing length field size in bytes
|
||||||
|
* @param isLE - whether length and state words are encoded in little-endian
|
||||||
|
* @example
|
||||||
|
* Use a concrete subclass to get the shared Merkle-Damgard update/digest flow.
|
||||||
|
* ```ts
|
||||||
|
* import { _SHA1 } from '@noble/hashes/legacy.js';
|
||||||
|
* const hash = new _SHA1();
|
||||||
|
* hash.update(new Uint8Array([97, 98, 99]));
|
||||||
|
* hash.digest();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class HashMD {
|
||||||
|
blockLen;
|
||||||
|
outputLen;
|
||||||
|
canXOF = false;
|
||||||
|
padOffset;
|
||||||
|
isLE;
|
||||||
|
// For partial updates less than block size
|
||||||
|
buffer;
|
||||||
|
view;
|
||||||
|
finished = false;
|
||||||
|
length = 0;
|
||||||
|
pos = 0;
|
||||||
|
destroyed = false;
|
||||||
|
constructor(blockLen, outputLen, padOffset, isLE) {
|
||||||
|
this.blockLen = blockLen;
|
||||||
|
this.outputLen = outputLen;
|
||||||
|
this.padOffset = padOffset;
|
||||||
|
this.isLE = isLE;
|
||||||
|
this.buffer = new Uint8Array(blockLen);
|
||||||
|
this.view = createView(this.buffer);
|
||||||
|
}
|
||||||
|
update(data) {
|
||||||
|
aexists(this);
|
||||||
|
abytes(data);
|
||||||
|
const { view, buffer, blockLen } = this;
|
||||||
|
const len = data.length;
|
||||||
|
for (let pos = 0; pos < len;) {
|
||||||
|
const take = Math.min(blockLen - this.pos, len - pos);
|
||||||
|
// Fast path only when there is no buffered partial block: `take === blockLen` implies
|
||||||
|
// `this.pos === 0`, so we can process full blocks directly from the input view.
|
||||||
|
if (take === blockLen) {
|
||||||
|
const dataView = createView(data);
|
||||||
|
for (; blockLen <= len - pos; pos += blockLen)
|
||||||
|
this.process(dataView, pos);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
buffer.set(data.subarray(pos, pos + take), this.pos);
|
||||||
|
this.pos += take;
|
||||||
|
pos += take;
|
||||||
|
if (this.pos === blockLen) {
|
||||||
|
this.process(view, 0);
|
||||||
|
this.pos = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.length += data.length;
|
||||||
|
this.roundClean();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
digestInto(out) {
|
||||||
|
aexists(this);
|
||||||
|
aoutput(out, this);
|
||||||
|
this.finished = true;
|
||||||
|
// Padding
|
||||||
|
// We can avoid allocation of buffer for padding completely if it
|
||||||
|
// was previously not allocated here. But it won't change performance.
|
||||||
|
const { buffer, view, blockLen, isLE } = this;
|
||||||
|
let { pos } = this;
|
||||||
|
// append the bit '1' to the message
|
||||||
|
buffer[pos++] = 0b10000000;
|
||||||
|
clean(this.buffer.subarray(pos));
|
||||||
|
// we have less than padOffset left in buffer, so we cannot put length in
|
||||||
|
// current block, need process it and pad again
|
||||||
|
if (this.padOffset > blockLen - pos) {
|
||||||
|
this.process(view, 0);
|
||||||
|
pos = 0;
|
||||||
|
}
|
||||||
|
// Pad until full block byte with zeros
|
||||||
|
for (let i = pos; i < blockLen; i++)
|
||||||
|
buffer[i] = 0;
|
||||||
|
// `padOffset` reserves the whole length field. For SHA-384/512 the high 64 bits stay zero from
|
||||||
|
// the padding fill above, and JS will overflow before user input can make that half non-zero.
|
||||||
|
// So we only need to write the low 64 bits here.
|
||||||
|
view.setBigUint64(blockLen - 8, BigInt(this.length * 8), isLE);
|
||||||
|
this.process(view, 0);
|
||||||
|
const oview = createView(out);
|
||||||
|
const len = this.outputLen;
|
||||||
|
// NOTE: we do division by 4 later, which must be fused in single op with modulo by JIT
|
||||||
|
if (len % 4)
|
||||||
|
throw new Error('_sha2: outputLen must be aligned to 32bit');
|
||||||
|
const outLen = len / 4;
|
||||||
|
const state = this.get();
|
||||||
|
if (outLen > state.length)
|
||||||
|
throw new Error('_sha2: outputLen bigger than state');
|
||||||
|
for (let i = 0; i < outLen; i++)
|
||||||
|
oview.setUint32(4 * i, state[i], isLE);
|
||||||
|
}
|
||||||
|
digest() {
|
||||||
|
const { buffer, outputLen } = this;
|
||||||
|
this.digestInto(buffer);
|
||||||
|
// Copy before destroy(): subclasses wipe `buffer` during cleanup, but `digest()` must return
|
||||||
|
// fresh bytes to the caller.
|
||||||
|
const res = buffer.slice(0, outputLen);
|
||||||
|
this.destroy();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
_cloneInto(to) {
|
||||||
|
to ||= new this.constructor();
|
||||||
|
to.set(...this.get());
|
||||||
|
const { blockLen, buffer, length, finished, destroyed, pos } = this;
|
||||||
|
to.destroyed = destroyed;
|
||||||
|
to.finished = finished;
|
||||||
|
to.length = length;
|
||||||
|
to.pos = pos;
|
||||||
|
// Only partial-block bytes need copying: when `length % blockLen === 0`, `pos === 0` and
|
||||||
|
// later `update()` / `digestInto()` overwrite `to.buffer` from the start before reading it.
|
||||||
|
if (length % blockLen)
|
||||||
|
to.buffer.set(buffer);
|
||||||
|
return to;
|
||||||
|
}
|
||||||
|
clone() {
|
||||||
|
return this._cloneInto();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Initial SHA-2 state: fractional parts of square roots of first 16 primes 2..53.
|
||||||
|
* Check out `test/misc/sha2-gen-iv.js` for recomputation guide.
|
||||||
|
*/
|
||||||
|
/** Initial SHA256 state from RFC 6234 §6.1: the first 32 bits of the fractional parts of the
|
||||||
|
* square roots of the first eight prime numbers. Exported as a shared table; callers must treat
|
||||||
|
* it as read-only because constructors copy words from it by index. */
|
||||||
|
export const SHA256_IV = /* @__PURE__ */ Uint32Array.from([
|
||||||
|
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
||||||
|
]);
|
||||||
|
/** Initial SHA224 state `H(0)` from RFC 6234 §6.1. Exported as a shared table; callers must
|
||||||
|
* treat it as read-only because constructors copy words from it by index. */
|
||||||
|
export const SHA224_IV = /* @__PURE__ */ Uint32Array.from([
|
||||||
|
0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4,
|
||||||
|
]);
|
||||||
|
/** Initial SHA384 state from RFC 6234 §6.3: eight RFC 64-bit `H(0)` words stored as sixteen
|
||||||
|
* big-endian 32-bit halves. Derived from the fractional parts of the square roots of the ninth
|
||||||
|
* through sixteenth prime numbers. Exported as a shared table; callers must treat it as read-only
|
||||||
|
* because constructors copy halves from it by index. */
|
||||||
|
export const SHA384_IV = /* @__PURE__ */ Uint32Array.from([
|
||||||
|
0xcbbb9d5d, 0xc1059ed8, 0x629a292a, 0x367cd507, 0x9159015a, 0x3070dd17, 0x152fecd8, 0xf70e5939,
|
||||||
|
0x67332667, 0xffc00b31, 0x8eb44a87, 0x68581511, 0xdb0c2e0d, 0x64f98fa7, 0x47b5481d, 0xbefa4fa4,
|
||||||
|
]);
|
||||||
|
/** Initial SHA512 state from RFC 6234 §6.3: eight RFC 64-bit `H(0)` words stored as sixteen
|
||||||
|
* big-endian 32-bit halves. Derived from the fractional parts of the square roots of the first
|
||||||
|
* eight prime numbers. Exported as a shared table; callers must treat it as read-only because
|
||||||
|
* constructors copy halves from it by index. */
|
||||||
|
export const SHA512_IV = /* @__PURE__ */ Uint32Array.from([
|
||||||
|
0x6a09e667, 0xf3bcc908, 0xbb67ae85, 0x84caa73b, 0x3c6ef372, 0xfe94f82b, 0xa54ff53a, 0x5f1d36f1,
|
||||||
|
0x510e527f, 0xade682d1, 0x9b05688c, 0x2b3e6c1f, 0x1f83d9ab, 0xfb41bd6b, 0x5be0cd19, 0x137e2179,
|
||||||
|
]);
|
||||||
|
//# sourceMappingURL=_md.js.map
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
const U32_MASK64 = /* @__PURE__ */ BigInt(2 ** 32 - 1);
|
||||||
|
const _32n = /* @__PURE__ */ BigInt(32);
|
||||||
|
// Split bigint into two 32-bit halves. With `le=true`, returned fields become `{ h: low, l: high
|
||||||
|
// }` to match little-endian word order rather than the property names.
|
||||||
|
function fromBig(n, le = false) {
|
||||||
|
if (le)
|
||||||
|
return { h: Number(n & U32_MASK64), l: Number((n >> _32n) & U32_MASK64) };
|
||||||
|
return { h: Number((n >> _32n) & U32_MASK64) | 0, l: Number(n & U32_MASK64) | 0 };
|
||||||
|
}
|
||||||
|
// Split bigint list into `[highWords, lowWords]` when `le=false`; with `le=true`, the first array
|
||||||
|
// holds the low halves because `fromBig(...)` swaps the semantic meaning of `h` and `l`.
|
||||||
|
function split(lst, le = false) {
|
||||||
|
const len = lst.length;
|
||||||
|
let Ah = new Uint32Array(len);
|
||||||
|
let Al = new Uint32Array(len);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const { h, l } = fromBig(lst[i], le);
|
||||||
|
[Ah[i], Al[i]] = [h, l];
|
||||||
|
}
|
||||||
|
return [Ah, Al];
|
||||||
|
}
|
||||||
|
// Combine explicit `(high, low)` 32-bit halves into a bigint; `>>> 0` normalizes signed JS
|
||||||
|
// bitwise results back to uint32 first, and little-endian callers must swap.
|
||||||
|
const toBig = (h, l) => (BigInt(h >>> 0) << _32n) | BigInt(l >>> 0);
|
||||||
|
// High 32-bit half of a 64-bit logical right shift for `s` in `0..31`.
|
||||||
|
const shrSH = (h, _l, s) => h >>> s;
|
||||||
|
// Low 32-bit half of a 64-bit logical right shift, valid for `s` in `1..31`.
|
||||||
|
const shrSL = (h, l, s) => (h << (32 - s)) | (l >>> s);
|
||||||
|
// High 32-bit half of a 64-bit right rotate, valid for `s` in `1..31`.
|
||||||
|
const rotrSH = (h, l, s) => (h >>> s) | (l << (32 - s));
|
||||||
|
// Low 32-bit half of a 64-bit right rotate, valid for `s` in `1..31`.
|
||||||
|
const rotrSL = (h, l, s) => (h << (32 - s)) | (l >>> s);
|
||||||
|
// High 32-bit half of a 64-bit right rotate, valid for `s` in `33..63`; `32` uses `rotr32*`.
|
||||||
|
const rotrBH = (h, l, s) => (h << (64 - s)) | (l >>> (s - 32));
|
||||||
|
// Low 32-bit half of a 64-bit right rotate, valid for `s` in `33..63`; `32` uses `rotr32*`.
|
||||||
|
const rotrBL = (h, l, s) => (h >>> (s - 32)) | (l << (64 - s));
|
||||||
|
// High 32-bit half of a 64-bit right rotate for `s === 32`; this is just the swapped low half.
|
||||||
|
const rotr32H = (_h, l) => l;
|
||||||
|
// Low 32-bit half of a 64-bit right rotate for `s === 32`; this is just the swapped high half.
|
||||||
|
const rotr32L = (h, _l) => h;
|
||||||
|
// High 32-bit half of a 64-bit left rotate, valid for `s` in `1..31`.
|
||||||
|
const rotlSH = (h, l, s) => (h << s) | (l >>> (32 - s));
|
||||||
|
// Low 32-bit half of a 64-bit left rotate, valid for `s` in `1..31`.
|
||||||
|
const rotlSL = (h, l, s) => (l << s) | (h >>> (32 - s));
|
||||||
|
// High 32-bit half of a 64-bit left rotate, valid for `s` in `33..63`; `32` uses `rotr32*`.
|
||||||
|
const rotlBH = (h, l, s) => (l << (s - 32)) | (h >>> (64 - s));
|
||||||
|
// Low 32-bit half of a 64-bit left rotate, valid for `s` in `33..63`; `32` uses `rotr32*`.
|
||||||
|
const rotlBL = (h, l, s) => (h << (s - 32)) | (l >>> (64 - s));
|
||||||
|
// Add two split 64-bit words and return the split `{ h, l }` sum.
|
||||||
|
// JS uses 32-bit signed integers for bitwise operations, so we cannot simply shift the carry out
|
||||||
|
// of the low sum and instead use division.
|
||||||
|
function add(Ah, Al, Bh, Bl) {
|
||||||
|
const l = (Al >>> 0) + (Bl >>> 0);
|
||||||
|
return { h: (Ah + Bh + ((l / 2 ** 32) | 0)) | 0, l: l | 0 };
|
||||||
|
}
|
||||||
|
// Addition with more than 2 elements
|
||||||
|
// Unmasked low-word accumulator for 3-way addition; pass the raw result into `add3H(...)`.
|
||||||
|
const add3L = (Al, Bl, Cl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0);
|
||||||
|
// High-word finalize step for 3-way addition; `low` must be the untruncated output of `add3L(...)`.
|
||||||
|
const add3H = (low, Ah, Bh, Ch) => (Ah + Bh + Ch + ((low / 2 ** 32) | 0)) | 0;
|
||||||
|
// Unmasked low-word accumulator for 4-way addition; pass the raw result into `add4H(...)`.
|
||||||
|
const add4L = (Al, Bl, Cl, Dl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0);
|
||||||
|
// High-word finalize step for 4-way addition; `low` must be the untruncated output of `add4L(...)`.
|
||||||
|
const add4H = (low, Ah, Bh, Ch, Dh) => (Ah + Bh + Ch + Dh + ((low / 2 ** 32) | 0)) | 0;
|
||||||
|
// Unmasked low-word accumulator for 5-way addition; pass the raw result into `add5H(...)`.
|
||||||
|
const add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0);
|
||||||
|
// High-word finalize step for 5-way addition; `low` must be the untruncated output of `add5L(...)`.
|
||||||
|
const add5H = (low, Ah, Bh, Ch, Dh, Eh) => (Ah + Bh + Ch + Dh + Eh + ((low / 2 ** 32) | 0)) | 0;
|
||||||
|
// prettier-ignore
|
||||||
|
export { add, add3H, add3L, add4H, add4L, add5H, add5L, fromBig, rotlBH, rotlBL, rotlSH, rotlSL, rotr32H, rotr32L, rotrBH, rotrBL, rotrSH, rotrSL, shrSH, shrSL, split, toBig };
|
||||||
|
// Canonical grouped namespace for callers that prefer one object.
|
||||||
|
// Named exports stay for direct imports.
|
||||||
|
// prettier-ignore
|
||||||
|
const u64 = {
|
||||||
|
fromBig, split, toBig,
|
||||||
|
shrSH, shrSL,
|
||||||
|
rotrSH, rotrSL, rotrBH, rotrBL,
|
||||||
|
rotr32H, rotr32L,
|
||||||
|
rotlSH, rotlSL, rotlBH, rotlBL,
|
||||||
|
add, add3L, add3H, add4L, add4H, add5H, add5L,
|
||||||
|
};
|
||||||
|
// Default export mirrors named `u64` for compatibility with object-style imports.
|
||||||
|
export default u64;
|
||||||
|
//# sourceMappingURL=_u64.js.map
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* HMAC: RFC2104 message authentication code.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
import { abytes, aexists, ahash, aoutput, clean, } from "./utils.js";
|
||||||
|
/**
|
||||||
|
* Internal class for HMAC.
|
||||||
|
* Accepts any byte key, although RFC 2104 §3 recommends keys at least
|
||||||
|
* `HashLen` bytes long.
|
||||||
|
*/
|
||||||
|
export class _HMAC {
|
||||||
|
oHash;
|
||||||
|
iHash;
|
||||||
|
blockLen;
|
||||||
|
outputLen;
|
||||||
|
canXOF = false;
|
||||||
|
finished = false;
|
||||||
|
destroyed = false;
|
||||||
|
constructor(hash, key) {
|
||||||
|
ahash(hash);
|
||||||
|
abytes(key, undefined, 'key');
|
||||||
|
this.iHash = hash.create();
|
||||||
|
if (typeof this.iHash.update !== 'function')
|
||||||
|
throw new Error('Expected instance of class which extends utils.Hash');
|
||||||
|
this.blockLen = this.iHash.blockLen;
|
||||||
|
this.outputLen = this.iHash.outputLen;
|
||||||
|
const blockLen = this.blockLen;
|
||||||
|
const pad = new Uint8Array(blockLen);
|
||||||
|
// blockLen can be bigger than outputLen
|
||||||
|
pad.set(key.length > blockLen ? hash.create().update(key).digest() : key);
|
||||||
|
for (let i = 0; i < pad.length; i++)
|
||||||
|
pad[i] ^= 0x36;
|
||||||
|
this.iHash.update(pad);
|
||||||
|
// By doing update (processing of the first block) of the outer hash here,
|
||||||
|
// we can re-use it between multiple calls via clone.
|
||||||
|
this.oHash = hash.create();
|
||||||
|
// Undo internal XOR && apply outer XOR
|
||||||
|
for (let i = 0; i < pad.length; i++)
|
||||||
|
pad[i] ^= 0x36 ^ 0x5c;
|
||||||
|
this.oHash.update(pad);
|
||||||
|
clean(pad);
|
||||||
|
}
|
||||||
|
update(buf) {
|
||||||
|
aexists(this);
|
||||||
|
this.iHash.update(buf);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
digestInto(out) {
|
||||||
|
aexists(this);
|
||||||
|
aoutput(out, this);
|
||||||
|
this.finished = true;
|
||||||
|
const buf = out.subarray(0, this.outputLen);
|
||||||
|
// Reuse the first outputLen bytes for the inner digest; the outer hash consumes them before
|
||||||
|
// overwriting that same prefix with the final tag, leaving any oversized tail untouched.
|
||||||
|
this.iHash.digestInto(buf);
|
||||||
|
this.oHash.update(buf);
|
||||||
|
this.oHash.digestInto(buf);
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
digest() {
|
||||||
|
const out = new Uint8Array(this.oHash.outputLen);
|
||||||
|
this.digestInto(out);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
_cloneInto(to) {
|
||||||
|
// Create new instance without calling constructor since the key
|
||||||
|
// is already in state and we don't know it.
|
||||||
|
to ||= Object.create(Object.getPrototypeOf(this), {});
|
||||||
|
const { oHash, iHash, finished, destroyed, blockLen, outputLen } = this;
|
||||||
|
to = to;
|
||||||
|
to.finished = finished;
|
||||||
|
to.destroyed = destroyed;
|
||||||
|
to.blockLen = blockLen;
|
||||||
|
to.outputLen = outputLen;
|
||||||
|
to.oHash = oHash._cloneInto(to.oHash);
|
||||||
|
to.iHash = iHash._cloneInto(to.iHash);
|
||||||
|
return to;
|
||||||
|
}
|
||||||
|
clone() {
|
||||||
|
return this._cloneInto();
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
this.destroyed = true;
|
||||||
|
this.oHash.destroy();
|
||||||
|
this.iHash.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const hmac = /* @__PURE__ */ (() => {
|
||||||
|
const hmac_ = ((hash, key, message) => new _HMAC(hash, key).update(message).digest());
|
||||||
|
hmac_.create = (hash, key) => new _HMAC(hash, key);
|
||||||
|
return hmac_;
|
||||||
|
})();
|
||||||
|
//# sourceMappingURL=hmac.js.map
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* PBKDF (RFC 2898). Can be used to create a key from password and salt.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
import { hmac } from "./hmac.js";
|
||||||
|
// prettier-ignore
|
||||||
|
import { ahash, anumber, asyncLoop, checkOpts, clean, createView, kdfInputToBytes } from "./utils.js";
|
||||||
|
// Common start and end for sync/async functions
|
||||||
|
function pbkdf2Init(hash, _password, _salt, _opts) {
|
||||||
|
ahash(hash);
|
||||||
|
const opts = checkOpts({ dkLen: 32, asyncTick: 10 }, _opts);
|
||||||
|
const { c, dkLen, asyncTick } = opts;
|
||||||
|
anumber(c, 'c');
|
||||||
|
anumber(dkLen, 'dkLen');
|
||||||
|
anumber(asyncTick, 'asyncTick');
|
||||||
|
if (c < 1)
|
||||||
|
throw new Error('iterations (c) must be >= 1');
|
||||||
|
// RFC 8018 §5.2 defines `dkLen` as "a positive integer".
|
||||||
|
if (dkLen < 1)
|
||||||
|
throw new Error('"dkLen" must be >= 1');
|
||||||
|
// RFC 8018 §5.2 step 1 requires rejecting oversize `dkLen`
|
||||||
|
// before allocating the destination buffer.
|
||||||
|
if (dkLen > (2 ** 32 - 1) * hash.outputLen)
|
||||||
|
throw new Error('derived key too long');
|
||||||
|
const password = kdfInputToBytes(_password, 'password');
|
||||||
|
const salt = kdfInputToBytes(_salt, 'salt');
|
||||||
|
// DK = PBKDF2(PRF, Password, Salt, c, dkLen);
|
||||||
|
const DK = new Uint8Array(dkLen);
|
||||||
|
// U1 = PRF(Password, Salt + INT_32_BE(i))
|
||||||
|
const PRF = hmac.create(hash, password);
|
||||||
|
// Cache PRF(P, S || ...) prefix state so each block only appends INT_32_BE(i).
|
||||||
|
const PRFSalt = PRF._cloneInto().update(salt);
|
||||||
|
return { c, dkLen, asyncTick, DK, PRF, PRFSalt };
|
||||||
|
}
|
||||||
|
function pbkdf2Output(PRF, PRFSalt, DK, prfW, u) {
|
||||||
|
// Shared sync/async cleanup point: wipe transient PRF state
|
||||||
|
// while preserving the derived key buffer.
|
||||||
|
PRF.destroy();
|
||||||
|
PRFSalt.destroy();
|
||||||
|
if (prfW)
|
||||||
|
prfW.destroy();
|
||||||
|
clean(u);
|
||||||
|
return DK;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* PBKDF2-HMAC: RFC 8018 key derivation function.
|
||||||
|
* @param hash - hash function that would be used e.g. sha256
|
||||||
|
* @param password - password from which a derived key is generated;
|
||||||
|
* JS string inputs are UTF-8 encoded first
|
||||||
|
* @param salt - cryptographic salt; JS string inputs are UTF-8 encoded first
|
||||||
|
* @param opts - PBKDF2 work factor and output settings. `dkLen`, if provided,
|
||||||
|
* must be `>= 1` per RFC 8018 §5.2. See {@link Pbkdf2Opt}.
|
||||||
|
* @returns Derived key bytes.
|
||||||
|
* @throws If the PBKDF2 iteration count or derived-key settings are invalid. {@link Error}
|
||||||
|
* @example
|
||||||
|
* PBKDF2-HMAC: RFC 2898 key derivation function.
|
||||||
|
* ```ts
|
||||||
|
* import { pbkdf2 } from '@noble/hashes/pbkdf2.js';
|
||||||
|
* import { sha256 } from '@noble/hashes/sha2.js';
|
||||||
|
* const key = pbkdf2(sha256, 'password', 'salt', { dkLen: 32, c: Math.pow(2, 18) });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function pbkdf2(hash, password, salt, opts) {
|
||||||
|
const { c, dkLen, DK, PRF, PRFSalt } = pbkdf2Init(hash, password, salt, opts);
|
||||||
|
let prfW; // Working copy
|
||||||
|
const arr = new Uint8Array(4);
|
||||||
|
const view = createView(arr);
|
||||||
|
const u = new Uint8Array(PRF.outputLen);
|
||||||
|
// DK = T1 + T2 + ⋯ + Tdklen/hlen
|
||||||
|
for (let ti = 1, pos = 0; pos < dkLen; ti++, pos += PRF.outputLen) {
|
||||||
|
// Ti = F(Password, Salt, c, i)
|
||||||
|
// The last Ti view can be shorter than hLen, which applies
|
||||||
|
// RFC 8018 §5.2 step 4's T_l<0..r-1> truncation without extra copies.
|
||||||
|
const Ti = DK.subarray(pos, pos + PRF.outputLen);
|
||||||
|
view.setInt32(0, ti, false);
|
||||||
|
// F(Password, Salt, c, i) = U1 ^ U2 ^ ⋯ ^ Uc
|
||||||
|
// U1 = PRF(Password, Salt + INT_32_BE(i))
|
||||||
|
(prfW = PRFSalt._cloneInto(prfW)).update(arr).digestInto(u);
|
||||||
|
Ti.set(u.subarray(0, Ti.length));
|
||||||
|
for (let ui = 1; ui < c; ui++) {
|
||||||
|
// Uc = PRF(Password, Uc−1)
|
||||||
|
PRF._cloneInto(prfW).update(u).digestInto(u);
|
||||||
|
for (let i = 0; i < Ti.length; i++)
|
||||||
|
Ti[i] ^= u[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pbkdf2Output(PRF, PRFSalt, DK, prfW, u);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* PBKDF2-HMAC: RFC 8018 key derivation function. Async version.
|
||||||
|
* @param hash - hash function that would be used e.g. sha256
|
||||||
|
* @param password - password from which a derived key is generated;
|
||||||
|
* JS string inputs are UTF-8 encoded first
|
||||||
|
* @param salt - cryptographic salt; JS string inputs are UTF-8 encoded first
|
||||||
|
* @param opts - PBKDF2 work factor and output settings. `dkLen`, if provided,
|
||||||
|
* must be `>= 1` per RFC 8018 §5.2. `asyncTick` is only a local
|
||||||
|
* scheduler-yield knob for this JS wrapper, not part of RFC 8018.
|
||||||
|
* See {@link Pbkdf2Opt}.
|
||||||
|
* @returns Promise resolving to derived key bytes.
|
||||||
|
* @throws If the PBKDF2 iteration count or derived-key settings are invalid. {@link Error}
|
||||||
|
* @example
|
||||||
|
* PBKDF2-HMAC: RFC 2898 key derivation function.
|
||||||
|
* ```ts
|
||||||
|
* import { pbkdf2Async } from '@noble/hashes/pbkdf2.js';
|
||||||
|
* import { sha256 } from '@noble/hashes/sha2.js';
|
||||||
|
* const key = await pbkdf2Async(sha256, 'password', 'salt', { dkLen: 32, c: 500_000 });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function pbkdf2Async(hash, password, salt, opts) {
|
||||||
|
const { c, dkLen, asyncTick, DK, PRF, PRFSalt } = pbkdf2Init(hash, password, salt, opts);
|
||||||
|
let prfW; // Working copy
|
||||||
|
const arr = new Uint8Array(4);
|
||||||
|
const view = createView(arr);
|
||||||
|
const u = new Uint8Array(PRF.outputLen);
|
||||||
|
// DK = T1 + T2 + ⋯ + Tdklen/hlen
|
||||||
|
for (let ti = 1, pos = 0; pos < dkLen; ti++, pos += PRF.outputLen) {
|
||||||
|
// Ti = F(Password, Salt, c, i)
|
||||||
|
// The last Ti view can be shorter than hLen, which applies
|
||||||
|
// RFC 8018 §5.2 step 4's T_l<0..r-1> truncation without extra copies.
|
||||||
|
const Ti = DK.subarray(pos, pos + PRF.outputLen);
|
||||||
|
view.setInt32(0, ti, false);
|
||||||
|
// F(Password, Salt, c, i) = U1 ^ U2 ^ ⋯ ^ Uc
|
||||||
|
// U1 = PRF(Password, Salt + INT_32_BE(i))
|
||||||
|
(prfW = PRFSalt._cloneInto(prfW)).update(arr).digestInto(u);
|
||||||
|
Ti.set(u.subarray(0, Ti.length));
|
||||||
|
await asyncLoop(c - 1, asyncTick, () => {
|
||||||
|
// Uc = PRF(Password, Uc−1)
|
||||||
|
PRF._cloneInto(prfW).update(u).digestInto(u);
|
||||||
|
for (let i = 0; i < Ti.length; i++)
|
||||||
|
Ti[i] ^= u[i];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return pbkdf2Output(PRF, PRFSalt, DK, prfW, u);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=pbkdf2.js.map
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
/**
|
||||||
|
* SHA2 hash function. A.k.a. sha256, sha384, sha512, sha512_224, sha512_256.
|
||||||
|
* SHA256 is the fastest hash implementable in JS, even faster than Blake3.
|
||||||
|
* Check out {@link https://www.rfc-editor.org/rfc/rfc4634 | RFC 4634} and
|
||||||
|
* {@link https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf | FIPS 180-4}.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
import { Chi, HashMD, Maj, SHA224_IV, SHA256_IV, SHA384_IV, SHA512_IV } from "./_md.js";
|
||||||
|
import * as u64 from "./_u64.js";
|
||||||
|
import { clean, createHasher, oidNist, rotr } from "./utils.js";
|
||||||
|
/**
|
||||||
|
* SHA-224 / SHA-256 round constants from RFC 6234 §5.1: the first 32 bits
|
||||||
|
* of the cube roots of the first 64 primes (2..311).
|
||||||
|
*/
|
||||||
|
// prettier-ignore
|
||||||
|
const SHA256_K = /* @__PURE__ */ Uint32Array.from([
|
||||||
|
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||||
|
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||||
|
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||||
|
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||||
|
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||||
|
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||||
|
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||||
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
||||||
|
]);
|
||||||
|
/** Reusable SHA-224 / SHA-256 message schedule buffer `W_t` from RFC 6234 §6.2 step 1. */
|
||||||
|
const SHA256_W = /* @__PURE__ */ new Uint32Array(64);
|
||||||
|
/** Internal SHA-224 / SHA-256 compression engine from RFC 6234 §6.2. */
|
||||||
|
class SHA2_32B extends HashMD {
|
||||||
|
constructor(outputLen) {
|
||||||
|
super(64, outputLen, 8, false);
|
||||||
|
}
|
||||||
|
get() {
|
||||||
|
const { A, B, C, D, E, F, G, H } = this;
|
||||||
|
return [A, B, C, D, E, F, G, H];
|
||||||
|
}
|
||||||
|
// prettier-ignore
|
||||||
|
set(A, B, C, D, E, F, G, H) {
|
||||||
|
this.A = A | 0;
|
||||||
|
this.B = B | 0;
|
||||||
|
this.C = C | 0;
|
||||||
|
this.D = D | 0;
|
||||||
|
this.E = E | 0;
|
||||||
|
this.F = F | 0;
|
||||||
|
this.G = G | 0;
|
||||||
|
this.H = H | 0;
|
||||||
|
}
|
||||||
|
process(view, offset) {
|
||||||
|
// Extend the first 16 words into the remaining 48 words w[16..63] of the message schedule array
|
||||||
|
for (let i = 0; i < 16; i++, offset += 4)
|
||||||
|
SHA256_W[i] = view.getUint32(offset, false);
|
||||||
|
for (let i = 16; i < 64; i++) {
|
||||||
|
const W15 = SHA256_W[i - 15];
|
||||||
|
const W2 = SHA256_W[i - 2];
|
||||||
|
const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ (W15 >>> 3);
|
||||||
|
const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ (W2 >>> 10);
|
||||||
|
SHA256_W[i] = (s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16]) | 0;
|
||||||
|
}
|
||||||
|
// Compression function main loop, 64 rounds
|
||||||
|
let { A, B, C, D, E, F, G, H } = this;
|
||||||
|
for (let i = 0; i < 64; i++) {
|
||||||
|
const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25);
|
||||||
|
const T1 = (H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i]) | 0;
|
||||||
|
const sigma0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22);
|
||||||
|
const T2 = (sigma0 + Maj(A, B, C)) | 0;
|
||||||
|
H = G;
|
||||||
|
G = F;
|
||||||
|
F = E;
|
||||||
|
E = (D + T1) | 0;
|
||||||
|
D = C;
|
||||||
|
C = B;
|
||||||
|
B = A;
|
||||||
|
A = (T1 + T2) | 0;
|
||||||
|
}
|
||||||
|
// Add the compressed chunk to the current hash value
|
||||||
|
A = (A + this.A) | 0;
|
||||||
|
B = (B + this.B) | 0;
|
||||||
|
C = (C + this.C) | 0;
|
||||||
|
D = (D + this.D) | 0;
|
||||||
|
E = (E + this.E) | 0;
|
||||||
|
F = (F + this.F) | 0;
|
||||||
|
G = (G + this.G) | 0;
|
||||||
|
H = (H + this.H) | 0;
|
||||||
|
this.set(A, B, C, D, E, F, G, H);
|
||||||
|
}
|
||||||
|
roundClean() {
|
||||||
|
clean(SHA256_W);
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
// HashMD callers route post-destroy usability through `destroyed`; zeroizing alone still leaves
|
||||||
|
// update()/digest() callable on reused instances.
|
||||||
|
this.destroyed = true;
|
||||||
|
this.set(0, 0, 0, 0, 0, 0, 0, 0);
|
||||||
|
clean(this.buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Internal SHA-256 hash class grounded in RFC 6234 §6.2. */
|
||||||
|
export class _SHA256 extends SHA2_32B {
|
||||||
|
// We cannot use array here since array allows indexing by variable
|
||||||
|
// which means optimizer/compiler cannot use registers.
|
||||||
|
A = SHA256_IV[0] | 0;
|
||||||
|
B = SHA256_IV[1] | 0;
|
||||||
|
C = SHA256_IV[2] | 0;
|
||||||
|
D = SHA256_IV[3] | 0;
|
||||||
|
E = SHA256_IV[4] | 0;
|
||||||
|
F = SHA256_IV[5] | 0;
|
||||||
|
G = SHA256_IV[6] | 0;
|
||||||
|
H = SHA256_IV[7] | 0;
|
||||||
|
constructor() {
|
||||||
|
super(32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Internal SHA-224 hash class grounded in RFC 6234 §6.2 and §8.5. */
|
||||||
|
export class _SHA224 extends SHA2_32B {
|
||||||
|
A = SHA224_IV[0] | 0;
|
||||||
|
B = SHA224_IV[1] | 0;
|
||||||
|
C = SHA224_IV[2] | 0;
|
||||||
|
D = SHA224_IV[3] | 0;
|
||||||
|
E = SHA224_IV[4] | 0;
|
||||||
|
F = SHA224_IV[5] | 0;
|
||||||
|
G = SHA224_IV[6] | 0;
|
||||||
|
H = SHA224_IV[7] | 0;
|
||||||
|
constructor() {
|
||||||
|
super(28);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SHA2-512 is slower than sha256 in js because u64 operations are slow.
|
||||||
|
// SHA-384 / SHA-512 round constants from RFC 6234 §5.2:
|
||||||
|
// 80 full 64-bit words split into high/low halves.
|
||||||
|
// prettier-ignore
|
||||||
|
const K512 = /* @__PURE__ */ (() => u64.split([
|
||||||
|
'0x428a2f98d728ae22', '0x7137449123ef65cd', '0xb5c0fbcfec4d3b2f', '0xe9b5dba58189dbbc',
|
||||||
|
'0x3956c25bf348b538', '0x59f111f1b605d019', '0x923f82a4af194f9b', '0xab1c5ed5da6d8118',
|
||||||
|
'0xd807aa98a3030242', '0x12835b0145706fbe', '0x243185be4ee4b28c', '0x550c7dc3d5ffb4e2',
|
||||||
|
'0x72be5d74f27b896f', '0x80deb1fe3b1696b1', '0x9bdc06a725c71235', '0xc19bf174cf692694',
|
||||||
|
'0xe49b69c19ef14ad2', '0xefbe4786384f25e3', '0x0fc19dc68b8cd5b5', '0x240ca1cc77ac9c65',
|
||||||
|
'0x2de92c6f592b0275', '0x4a7484aa6ea6e483', '0x5cb0a9dcbd41fbd4', '0x76f988da831153b5',
|
||||||
|
'0x983e5152ee66dfab', '0xa831c66d2db43210', '0xb00327c898fb213f', '0xbf597fc7beef0ee4',
|
||||||
|
'0xc6e00bf33da88fc2', '0xd5a79147930aa725', '0x06ca6351e003826f', '0x142929670a0e6e70',
|
||||||
|
'0x27b70a8546d22ffc', '0x2e1b21385c26c926', '0x4d2c6dfc5ac42aed', '0x53380d139d95b3df',
|
||||||
|
'0x650a73548baf63de', '0x766a0abb3c77b2a8', '0x81c2c92e47edaee6', '0x92722c851482353b',
|
||||||
|
'0xa2bfe8a14cf10364', '0xa81a664bbc423001', '0xc24b8b70d0f89791', '0xc76c51a30654be30',
|
||||||
|
'0xd192e819d6ef5218', '0xd69906245565a910', '0xf40e35855771202a', '0x106aa07032bbd1b8',
|
||||||
|
'0x19a4c116b8d2d0c8', '0x1e376c085141ab53', '0x2748774cdf8eeb99', '0x34b0bcb5e19b48a8',
|
||||||
|
'0x391c0cb3c5c95a63', '0x4ed8aa4ae3418acb', '0x5b9cca4f7763e373', '0x682e6ff3d6b2b8a3',
|
||||||
|
'0x748f82ee5defb2fc', '0x78a5636f43172f60', '0x84c87814a1f0ab72', '0x8cc702081a6439ec',
|
||||||
|
'0x90befffa23631e28', '0xa4506cebde82bde9', '0xbef9a3f7b2c67915', '0xc67178f2e372532b',
|
||||||
|
'0xca273eceea26619c', '0xd186b8c721c0c207', '0xeada7dd6cde0eb1e', '0xf57d4f7fee6ed178',
|
||||||
|
'0x06f067aa72176fba', '0x0a637dc5a2c898a6', '0x113f9804bef90dae', '0x1b710b35131c471b',
|
||||||
|
'0x28db77f523047d84', '0x32caab7b40c72493', '0x3c9ebe0a15c9bebc', '0x431d67c49c100d4c',
|
||||||
|
'0x4cc5d4becb3e42b6', '0x597f299cfc657e2a', '0x5fcb6fab3ad6faec', '0x6c44198c4a475817'
|
||||||
|
].map(n => BigInt(n))))();
|
||||||
|
const SHA512_Kh = /* @__PURE__ */ (() => K512[0])();
|
||||||
|
const SHA512_Kl = /* @__PURE__ */ (() => K512[1])();
|
||||||
|
// Reusable high-half schedule buffer for the RFC 6234 §6.4 64-bit `W_t` words.
|
||||||
|
const SHA512_W_H = /* @__PURE__ */ new Uint32Array(80);
|
||||||
|
// Reusable low-half schedule buffer for the RFC 6234 §6.4 64-bit `W_t` words.
|
||||||
|
const SHA512_W_L = /* @__PURE__ */ new Uint32Array(80);
|
||||||
|
/** Internal SHA-384 / SHA-512 compression engine from RFC 6234 §6.4. */
|
||||||
|
class SHA2_64B extends HashMD {
|
||||||
|
constructor(outputLen) {
|
||||||
|
super(128, outputLen, 16, false);
|
||||||
|
}
|
||||||
|
// prettier-ignore
|
||||||
|
get() {
|
||||||
|
const { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
|
||||||
|
return [Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl];
|
||||||
|
}
|
||||||
|
// prettier-ignore
|
||||||
|
set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl) {
|
||||||
|
this.Ah = Ah | 0;
|
||||||
|
this.Al = Al | 0;
|
||||||
|
this.Bh = Bh | 0;
|
||||||
|
this.Bl = Bl | 0;
|
||||||
|
this.Ch = Ch | 0;
|
||||||
|
this.Cl = Cl | 0;
|
||||||
|
this.Dh = Dh | 0;
|
||||||
|
this.Dl = Dl | 0;
|
||||||
|
this.Eh = Eh | 0;
|
||||||
|
this.El = El | 0;
|
||||||
|
this.Fh = Fh | 0;
|
||||||
|
this.Fl = Fl | 0;
|
||||||
|
this.Gh = Gh | 0;
|
||||||
|
this.Gl = Gl | 0;
|
||||||
|
this.Hh = Hh | 0;
|
||||||
|
this.Hl = Hl | 0;
|
||||||
|
}
|
||||||
|
process(view, offset) {
|
||||||
|
// Extend the first 16 words into the remaining 64 words w[16..79] of the message schedule array
|
||||||
|
for (let i = 0; i < 16; i++, offset += 4) {
|
||||||
|
SHA512_W_H[i] = view.getUint32(offset);
|
||||||
|
SHA512_W_L[i] = view.getUint32((offset += 4));
|
||||||
|
}
|
||||||
|
for (let i = 16; i < 80; i++) {
|
||||||
|
// s0 := (w[i-15] rightrotate 1) xor (w[i-15] rightrotate 8) xor (w[i-15] rightshift 7)
|
||||||
|
const W15h = SHA512_W_H[i - 15] | 0;
|
||||||
|
const W15l = SHA512_W_L[i - 15] | 0;
|
||||||
|
const s0h = u64.rotrSH(W15h, W15l, 1) ^ u64.rotrSH(W15h, W15l, 8) ^ u64.shrSH(W15h, W15l, 7);
|
||||||
|
const s0l = u64.rotrSL(W15h, W15l, 1) ^ u64.rotrSL(W15h, W15l, 8) ^ u64.shrSL(W15h, W15l, 7);
|
||||||
|
// s1 := (w[i-2] rightrotate 19) xor (w[i-2] rightrotate 61) xor (w[i-2] rightshift 6)
|
||||||
|
const W2h = SHA512_W_H[i - 2] | 0;
|
||||||
|
const W2l = SHA512_W_L[i - 2] | 0;
|
||||||
|
const s1h = u64.rotrSH(W2h, W2l, 19) ^ u64.rotrBH(W2h, W2l, 61) ^ u64.shrSH(W2h, W2l, 6);
|
||||||
|
const s1l = u64.rotrSL(W2h, W2l, 19) ^ u64.rotrBL(W2h, W2l, 61) ^ u64.shrSL(W2h, W2l, 6);
|
||||||
|
// SHA512_W[i] = s0 + s1 + SHA512_W[i - 7] + SHA512_W[i - 16];
|
||||||
|
const SUMl = u64.add4L(s0l, s1l, SHA512_W_L[i - 7], SHA512_W_L[i - 16]);
|
||||||
|
const SUMh = u64.add4H(SUMl, s0h, s1h, SHA512_W_H[i - 7], SHA512_W_H[i - 16]);
|
||||||
|
SHA512_W_H[i] = SUMh | 0;
|
||||||
|
SHA512_W_L[i] = SUMl | 0;
|
||||||
|
}
|
||||||
|
let { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
|
||||||
|
// Compression function main loop, 80 rounds
|
||||||
|
for (let i = 0; i < 80; i++) {
|
||||||
|
// S1 := (e rightrotate 14) xor (e rightrotate 18) xor (e rightrotate 41)
|
||||||
|
const sigma1h = u64.rotrSH(Eh, El, 14) ^ u64.rotrSH(Eh, El, 18) ^ u64.rotrBH(Eh, El, 41);
|
||||||
|
const sigma1l = u64.rotrSL(Eh, El, 14) ^ u64.rotrSL(Eh, El, 18) ^ u64.rotrBL(Eh, El, 41);
|
||||||
|
//const T1 = (H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i]) | 0;
|
||||||
|
const CHIh = (Eh & Fh) ^ (~Eh & Gh);
|
||||||
|
const CHIl = (El & Fl) ^ (~El & Gl);
|
||||||
|
// T1 = H + sigma1 + Chi(E, F, G) + SHA512_K[i] + SHA512_W[i]
|
||||||
|
// prettier-ignore
|
||||||
|
const T1ll = u64.add5L(Hl, sigma1l, CHIl, SHA512_Kl[i], SHA512_W_L[i]);
|
||||||
|
const T1h = u64.add5H(T1ll, Hh, sigma1h, CHIh, SHA512_Kh[i], SHA512_W_H[i]);
|
||||||
|
const T1l = T1ll | 0;
|
||||||
|
// S0 := (a rightrotate 28) xor (a rightrotate 34) xor (a rightrotate 39)
|
||||||
|
const sigma0h = u64.rotrSH(Ah, Al, 28) ^ u64.rotrBH(Ah, Al, 34) ^ u64.rotrBH(Ah, Al, 39);
|
||||||
|
const sigma0l = u64.rotrSL(Ah, Al, 28) ^ u64.rotrBL(Ah, Al, 34) ^ u64.rotrBL(Ah, Al, 39);
|
||||||
|
const MAJh = (Ah & Bh) ^ (Ah & Ch) ^ (Bh & Ch);
|
||||||
|
const MAJl = (Al & Bl) ^ (Al & Cl) ^ (Bl & Cl);
|
||||||
|
Hh = Gh | 0;
|
||||||
|
Hl = Gl | 0;
|
||||||
|
Gh = Fh | 0;
|
||||||
|
Gl = Fl | 0;
|
||||||
|
Fh = Eh | 0;
|
||||||
|
Fl = El | 0;
|
||||||
|
({ h: Eh, l: El } = u64.add(Dh | 0, Dl | 0, T1h | 0, T1l | 0));
|
||||||
|
Dh = Ch | 0;
|
||||||
|
Dl = Cl | 0;
|
||||||
|
Ch = Bh | 0;
|
||||||
|
Cl = Bl | 0;
|
||||||
|
Bh = Ah | 0;
|
||||||
|
Bl = Al | 0;
|
||||||
|
const All = u64.add3L(T1l, sigma0l, MAJl);
|
||||||
|
Ah = u64.add3H(All, T1h, sigma0h, MAJh);
|
||||||
|
Al = All | 0;
|
||||||
|
}
|
||||||
|
// Add the compressed chunk to the current hash value
|
||||||
|
({ h: Ah, l: Al } = u64.add(this.Ah | 0, this.Al | 0, Ah | 0, Al | 0));
|
||||||
|
({ h: Bh, l: Bl } = u64.add(this.Bh | 0, this.Bl | 0, Bh | 0, Bl | 0));
|
||||||
|
({ h: Ch, l: Cl } = u64.add(this.Ch | 0, this.Cl | 0, Ch | 0, Cl | 0));
|
||||||
|
({ h: Dh, l: Dl } = u64.add(this.Dh | 0, this.Dl | 0, Dh | 0, Dl | 0));
|
||||||
|
({ h: Eh, l: El } = u64.add(this.Eh | 0, this.El | 0, Eh | 0, El | 0));
|
||||||
|
({ h: Fh, l: Fl } = u64.add(this.Fh | 0, this.Fl | 0, Fh | 0, Fl | 0));
|
||||||
|
({ h: Gh, l: Gl } = u64.add(this.Gh | 0, this.Gl | 0, Gh | 0, Gl | 0));
|
||||||
|
({ h: Hh, l: Hl } = u64.add(this.Hh | 0, this.Hl | 0, Hh | 0, Hl | 0));
|
||||||
|
this.set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl);
|
||||||
|
}
|
||||||
|
roundClean() {
|
||||||
|
clean(SHA512_W_H, SHA512_W_L);
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
// HashMD callers route post-destroy usability through `destroyed`; zeroizing alone still leaves
|
||||||
|
// update()/digest() callable on reused instances.
|
||||||
|
this.destroyed = true;
|
||||||
|
clean(this.buffer);
|
||||||
|
this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Internal SHA-512 hash class grounded in RFC 6234 §6.3 and §6.4. */
|
||||||
|
export class _SHA512 extends SHA2_64B {
|
||||||
|
Ah = SHA512_IV[0] | 0;
|
||||||
|
Al = SHA512_IV[1] | 0;
|
||||||
|
Bh = SHA512_IV[2] | 0;
|
||||||
|
Bl = SHA512_IV[3] | 0;
|
||||||
|
Ch = SHA512_IV[4] | 0;
|
||||||
|
Cl = SHA512_IV[5] | 0;
|
||||||
|
Dh = SHA512_IV[6] | 0;
|
||||||
|
Dl = SHA512_IV[7] | 0;
|
||||||
|
Eh = SHA512_IV[8] | 0;
|
||||||
|
El = SHA512_IV[9] | 0;
|
||||||
|
Fh = SHA512_IV[10] | 0;
|
||||||
|
Fl = SHA512_IV[11] | 0;
|
||||||
|
Gh = SHA512_IV[12] | 0;
|
||||||
|
Gl = SHA512_IV[13] | 0;
|
||||||
|
Hh = SHA512_IV[14] | 0;
|
||||||
|
Hl = SHA512_IV[15] | 0;
|
||||||
|
constructor() {
|
||||||
|
super(64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Internal SHA-384 hash class grounded in RFC 6234 §6.3 and §6.4. */
|
||||||
|
export class _SHA384 extends SHA2_64B {
|
||||||
|
Ah = SHA384_IV[0] | 0;
|
||||||
|
Al = SHA384_IV[1] | 0;
|
||||||
|
Bh = SHA384_IV[2] | 0;
|
||||||
|
Bl = SHA384_IV[3] | 0;
|
||||||
|
Ch = SHA384_IV[4] | 0;
|
||||||
|
Cl = SHA384_IV[5] | 0;
|
||||||
|
Dh = SHA384_IV[6] | 0;
|
||||||
|
Dl = SHA384_IV[7] | 0;
|
||||||
|
Eh = SHA384_IV[8] | 0;
|
||||||
|
El = SHA384_IV[9] | 0;
|
||||||
|
Fh = SHA384_IV[10] | 0;
|
||||||
|
Fl = SHA384_IV[11] | 0;
|
||||||
|
Gh = SHA384_IV[12] | 0;
|
||||||
|
Gl = SHA384_IV[13] | 0;
|
||||||
|
Hh = SHA384_IV[14] | 0;
|
||||||
|
Hl = SHA384_IV[15] | 0;
|
||||||
|
constructor() {
|
||||||
|
super(48);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Truncated SHA512/256 and SHA512/224.
|
||||||
|
* SHA512_IV is XORed with 0xa5a5a5a5a5a5a5a5, then used as "intermediary" IV of SHA512/t.
|
||||||
|
* Then t hashes string to produce result IV.
|
||||||
|
* See the repo-side derivation recipe in `test/misc/sha2-gen-iv.js`.
|
||||||
|
* These IV literals are checked against that script rather than a dedicated
|
||||||
|
* local RFC section.
|
||||||
|
*/
|
||||||
|
/** SHA-512/224 IV derived by the SHA-512/t recipe in `test/misc/sha2-gen-iv.js` and
|
||||||
|
* stored as sixteen big-endian 32-bit halves. */
|
||||||
|
const T224_IV = /* @__PURE__ */ Uint32Array.from([
|
||||||
|
0x8c3d37c8, 0x19544da2, 0x73e19966, 0x89dcd4d6, 0x1dfab7ae, 0x32ff9c82, 0x679dd514, 0x582f9fcf,
|
||||||
|
0x0f6d2b69, 0x7bd44da8, 0x77e36f73, 0x04c48942, 0x3f9d85a8, 0x6a1d36c8, 0x1112e6ad, 0x91d692a1,
|
||||||
|
]);
|
||||||
|
/** SHA-512/256 IV derived by the SHA-512/t recipe in `test/misc/sha2-gen-iv.js` and
|
||||||
|
* stored as sixteen big-endian 32-bit halves. */
|
||||||
|
const T256_IV = /* @__PURE__ */ Uint32Array.from([
|
||||||
|
0x22312194, 0xfc2bf72c, 0x9f555fa3, 0xc84c64c2, 0x2393b86b, 0x6f53b151, 0x96387719, 0x5940eabd,
|
||||||
|
0x96283ee2, 0xa88effe3, 0xbe5e1e25, 0x53863992, 0x2b0199fc, 0x2c85b8aa, 0x0eb72ddc, 0x81c52ca2,
|
||||||
|
]);
|
||||||
|
/** Internal SHA-512/224 hash class using the derived `T224_IV` and the shared
|
||||||
|
* RFC 6234 §6.4 compression engine. */
|
||||||
|
export class _SHA512_224 extends SHA2_64B {
|
||||||
|
Ah = T224_IV[0] | 0;
|
||||||
|
Al = T224_IV[1] | 0;
|
||||||
|
Bh = T224_IV[2] | 0;
|
||||||
|
Bl = T224_IV[3] | 0;
|
||||||
|
Ch = T224_IV[4] | 0;
|
||||||
|
Cl = T224_IV[5] | 0;
|
||||||
|
Dh = T224_IV[6] | 0;
|
||||||
|
Dl = T224_IV[7] | 0;
|
||||||
|
Eh = T224_IV[8] | 0;
|
||||||
|
El = T224_IV[9] | 0;
|
||||||
|
Fh = T224_IV[10] | 0;
|
||||||
|
Fl = T224_IV[11] | 0;
|
||||||
|
Gh = T224_IV[12] | 0;
|
||||||
|
Gl = T224_IV[13] | 0;
|
||||||
|
Hh = T224_IV[14] | 0;
|
||||||
|
Hl = T224_IV[15] | 0;
|
||||||
|
constructor() {
|
||||||
|
super(28);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Internal SHA-512/256 hash class using the derived `T256_IV` and the shared
|
||||||
|
* RFC 6234 §6.4 compression engine. */
|
||||||
|
export class _SHA512_256 extends SHA2_64B {
|
||||||
|
Ah = T256_IV[0] | 0;
|
||||||
|
Al = T256_IV[1] | 0;
|
||||||
|
Bh = T256_IV[2] | 0;
|
||||||
|
Bl = T256_IV[3] | 0;
|
||||||
|
Ch = T256_IV[4] | 0;
|
||||||
|
Cl = T256_IV[5] | 0;
|
||||||
|
Dh = T256_IV[6] | 0;
|
||||||
|
Dl = T256_IV[7] | 0;
|
||||||
|
Eh = T256_IV[8] | 0;
|
||||||
|
El = T256_IV[9] | 0;
|
||||||
|
Fh = T256_IV[10] | 0;
|
||||||
|
Fl = T256_IV[11] | 0;
|
||||||
|
Gh = T256_IV[12] | 0;
|
||||||
|
Gl = T256_IV[13] | 0;
|
||||||
|
Hh = T256_IV[14] | 0;
|
||||||
|
Hl = T256_IV[15] | 0;
|
||||||
|
constructor() {
|
||||||
|
super(32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* SHA2-256 hash function from RFC 4634. In JS it's the fastest: even faster than Blake3. Some info:
|
||||||
|
*
|
||||||
|
* - Trying 2^128 hashes would get 50% chance of collision, using birthday attack.
|
||||||
|
* - BTC network is doing 2^70 hashes/sec (2^95 hashes/year) as per 2025.
|
||||||
|
* - Each sha256 hash is executing 2^18 bit operations.
|
||||||
|
* - Good 2024 ASICs can do 200Th/sec with 3500 watts of power, corresponding to 2^36 hashes/joule.
|
||||||
|
* @param msg - message bytes to hash
|
||||||
|
* @returns Digest bytes.
|
||||||
|
* @example
|
||||||
|
* Hash a message with SHA2-256.
|
||||||
|
* ```ts
|
||||||
|
* sha256(new Uint8Array([97, 98, 99]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const sha256 = /* @__PURE__ */ createHasher(() => new _SHA256(),
|
||||||
|
/* @__PURE__ */ oidNist(0x01));
|
||||||
|
/**
|
||||||
|
* SHA2-224 hash function from RFC 4634.
|
||||||
|
* @param msg - message bytes to hash
|
||||||
|
* @returns Digest bytes.
|
||||||
|
* @example
|
||||||
|
* Hash a message with SHA2-224.
|
||||||
|
* ```ts
|
||||||
|
* sha224(new Uint8Array([97, 98, 99]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const sha224 = /* @__PURE__ */ createHasher(() => new _SHA224(),
|
||||||
|
/* @__PURE__ */ oidNist(0x04));
|
||||||
|
/**
|
||||||
|
* SHA2-512 hash function from RFC 4634.
|
||||||
|
* @param msg - message bytes to hash
|
||||||
|
* @returns Digest bytes.
|
||||||
|
* @example
|
||||||
|
* Hash a message with SHA2-512.
|
||||||
|
* ```ts
|
||||||
|
* sha512(new Uint8Array([97, 98, 99]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const sha512 = /* @__PURE__ */ createHasher(() => new _SHA512(),
|
||||||
|
/* @__PURE__ */ oidNist(0x03));
|
||||||
|
/**
|
||||||
|
* SHA2-384 hash function from RFC 4634.
|
||||||
|
* @param msg - message bytes to hash
|
||||||
|
* @returns Digest bytes.
|
||||||
|
* @example
|
||||||
|
* Hash a message with SHA2-384.
|
||||||
|
* ```ts
|
||||||
|
* sha384(new Uint8Array([97, 98, 99]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const sha384 = /* @__PURE__ */ createHasher(() => new _SHA384(),
|
||||||
|
/* @__PURE__ */ oidNist(0x02));
|
||||||
|
/**
|
||||||
|
* SHA2-512/256 "truncated" hash function, with improved resistance to length extension attacks.
|
||||||
|
* See the paper on {@link https://eprint.iacr.org/2010/548.pdf | truncated SHA512}.
|
||||||
|
* @param msg - message bytes to hash
|
||||||
|
* @returns Digest bytes.
|
||||||
|
* @example
|
||||||
|
* Hash a message with SHA2-512/256.
|
||||||
|
* ```ts
|
||||||
|
* sha512_256(new Uint8Array([97, 98, 99]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const sha512_256 = /* @__PURE__ */ createHasher(() => new _SHA512_256(),
|
||||||
|
/* @__PURE__ */ oidNist(0x06));
|
||||||
|
/**
|
||||||
|
* SHA2-512/224 "truncated" hash function, with improved resistance to length extension attacks.
|
||||||
|
* See the paper on {@link https://eprint.iacr.org/2010/548.pdf | truncated SHA512}.
|
||||||
|
* @param msg - message bytes to hash
|
||||||
|
* @returns Digest bytes.
|
||||||
|
* @example
|
||||||
|
* Hash a message with SHA2-512/224.
|
||||||
|
* ```ts
|
||||||
|
* sha512_224(new Uint8Array([97, 98, 99]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const sha512_224 = /* @__PURE__ */ createHasher(() => new _SHA512_224(),
|
||||||
|
/* @__PURE__ */ oidNist(0x05));
|
||||||
|
//# sourceMappingURL=sha2.js.map
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
/**
|
||||||
|
* Checks if something is Uint8Array. Be careful: nodejs Buffer will return true.
|
||||||
|
* @param a - value to test
|
||||||
|
* @returns `true` when the value is a Uint8Array-compatible view.
|
||||||
|
* @example
|
||||||
|
* Check whether a value is a Uint8Array-compatible view.
|
||||||
|
* ```ts
|
||||||
|
* isBytes(new Uint8Array([1, 2, 3]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function isBytes(a) {
|
||||||
|
// Plain `instanceof Uint8Array` is too strict for some Buffer / proxy / cross-realm cases.
|
||||||
|
// The fallback still requires a real ArrayBuffer view, so plain
|
||||||
|
// JSON-deserialized `{ constructor: ... }` spoofing is rejected, and
|
||||||
|
// `BYTES_PER_ELEMENT === 1` keeps the fallback on byte-oriented views.
|
||||||
|
return (a instanceof Uint8Array ||
|
||||||
|
(ArrayBuffer.isView(a) &&
|
||||||
|
a.constructor.name === 'Uint8Array' &&
|
||||||
|
'BYTES_PER_ELEMENT' in a &&
|
||||||
|
a.BYTES_PER_ELEMENT === 1));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Asserts something is a non-negative integer.
|
||||||
|
* @param n - number to validate
|
||||||
|
* @param title - label included in thrown errors
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @throws On wrong argument ranges or values. {@link RangeError}
|
||||||
|
* @example
|
||||||
|
* Validate a non-negative integer option.
|
||||||
|
* ```ts
|
||||||
|
* anumber(32, 'length');
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function anumber(n, title = '') {
|
||||||
|
if (typeof n !== 'number') {
|
||||||
|
const prefix = title && `"${title}" `;
|
||||||
|
throw new TypeError(`${prefix}expected number, got ${typeof n}`);
|
||||||
|
}
|
||||||
|
if (!Number.isSafeInteger(n) || n < 0) {
|
||||||
|
const prefix = title && `"${title}" `;
|
||||||
|
throw new RangeError(`${prefix}expected integer >= 0, got ${n}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Asserts something is Uint8Array.
|
||||||
|
* @param value - value to validate
|
||||||
|
* @param length - optional exact length constraint
|
||||||
|
* @param title - label included in thrown errors
|
||||||
|
* @returns The validated byte array.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @throws On wrong argument ranges or values. {@link RangeError}
|
||||||
|
* @example
|
||||||
|
* Validate that a value is a byte array.
|
||||||
|
* ```ts
|
||||||
|
* abytes(new Uint8Array([1, 2, 3]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function abytes(value, length, title = '') {
|
||||||
|
const bytes = isBytes(value);
|
||||||
|
const len = value?.length;
|
||||||
|
const needsLen = length !== undefined;
|
||||||
|
if (!bytes || (needsLen && len !== length)) {
|
||||||
|
const prefix = title && `"${title}" `;
|
||||||
|
const ofLen = needsLen ? ` of length ${length}` : '';
|
||||||
|
const got = bytes ? `length=${len}` : `type=${typeof value}`;
|
||||||
|
const message = prefix + 'expected Uint8Array' + ofLen + ', got ' + got;
|
||||||
|
if (!bytes)
|
||||||
|
throw new TypeError(message);
|
||||||
|
throw new RangeError(message);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Copies bytes into a fresh Uint8Array.
|
||||||
|
* Buffer-style slices can alias the same backing store, so callers that need ownership should copy.
|
||||||
|
* @param bytes - source bytes to clone
|
||||||
|
* @returns Freshly allocated copy of `bytes`.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @example
|
||||||
|
* Clone a byte array before mutating it.
|
||||||
|
* ```ts
|
||||||
|
* const copy = copyBytes(new Uint8Array([1, 2, 3]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function copyBytes(bytes) {
|
||||||
|
// `Uint8Array.from(...)` would also accept arrays / other typed arrays. Keep this helper strict
|
||||||
|
// because callers use it at byte-validation boundaries before mutating the detached copy.
|
||||||
|
return Uint8Array.from(abytes(bytes));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Asserts something is a wrapped hash constructor.
|
||||||
|
* @param h - hash constructor to validate
|
||||||
|
* @throws On wrong argument types or invalid hash wrapper shape. {@link TypeError}
|
||||||
|
* @throws On invalid hash metadata ranges or values. {@link RangeError}
|
||||||
|
* @throws If the hash metadata allows empty outputs or block sizes. {@link Error}
|
||||||
|
* @example
|
||||||
|
* Validate a callable hash wrapper.
|
||||||
|
* ```ts
|
||||||
|
* import { ahash } from '@noble/hashes/utils.js';
|
||||||
|
* import { sha256 } from '@noble/hashes/sha2.js';
|
||||||
|
* ahash(sha256);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function ahash(h) {
|
||||||
|
if (typeof h !== 'function' || typeof h.create !== 'function')
|
||||||
|
throw new TypeError('Hash must wrapped by utils.createHasher');
|
||||||
|
anumber(h.outputLen);
|
||||||
|
anumber(h.blockLen);
|
||||||
|
// HMAC and KDF callers treat these as real byte lengths; allowing zero lets fake wrappers pass
|
||||||
|
// validation and can produce empty outputs instead of failing fast.
|
||||||
|
if (h.outputLen < 1)
|
||||||
|
throw new Error('"outputLen" must be >= 1');
|
||||||
|
if (h.blockLen < 1)
|
||||||
|
throw new Error('"blockLen" must be >= 1');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Asserts a hash instance has not been destroyed or finished.
|
||||||
|
* @param instance - hash instance to validate
|
||||||
|
* @param checkFinished - whether to reject finalized instances
|
||||||
|
* @throws If the hash instance has already been destroyed or finalized. {@link Error}
|
||||||
|
* @example
|
||||||
|
* Validate that a hash instance is still usable.
|
||||||
|
* ```ts
|
||||||
|
* import { aexists } from '@noble/hashes/utils.js';
|
||||||
|
* import { sha256 } from '@noble/hashes/sha2.js';
|
||||||
|
* const hash = sha256.create();
|
||||||
|
* aexists(hash);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function aexists(instance, checkFinished = true) {
|
||||||
|
if (instance.destroyed)
|
||||||
|
throw new Error('Hash instance has been destroyed');
|
||||||
|
if (checkFinished && instance.finished)
|
||||||
|
throw new Error('Hash#digest() has already been called');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Asserts output is a sufficiently-sized byte array.
|
||||||
|
* @param out - destination buffer
|
||||||
|
* @param instance - hash instance providing output length
|
||||||
|
* Oversized buffers are allowed; downstream code only promises to fill the first `outputLen` bytes.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @throws On wrong argument ranges or values. {@link RangeError}
|
||||||
|
* @example
|
||||||
|
* Validate a caller-provided digest buffer.
|
||||||
|
* ```ts
|
||||||
|
* import { aoutput } from '@noble/hashes/utils.js';
|
||||||
|
* import { sha256 } from '@noble/hashes/sha2.js';
|
||||||
|
* const hash = sha256.create();
|
||||||
|
* aoutput(new Uint8Array(hash.outputLen), hash);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function aoutput(out, instance) {
|
||||||
|
abytes(out, undefined, 'digestInto() output');
|
||||||
|
const min = instance.outputLen;
|
||||||
|
if (out.length < min) {
|
||||||
|
throw new RangeError('"digestInto() output" expected to be of length >=' + min);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Casts a typed array view to Uint8Array.
|
||||||
|
* @param arr - source typed array
|
||||||
|
* @returns Uint8Array view over the same buffer.
|
||||||
|
* @example
|
||||||
|
* Reinterpret a typed array as bytes.
|
||||||
|
* ```ts
|
||||||
|
* u8(new Uint32Array([1, 2]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function u8(arr) {
|
||||||
|
return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Casts a typed array view to Uint32Array.
|
||||||
|
* `arr.byteOffset` must already be 4-byte aligned or the platform
|
||||||
|
* Uint32Array constructor will throw.
|
||||||
|
* @param arr - source typed array
|
||||||
|
* @returns Uint32Array view over the same buffer.
|
||||||
|
* @example
|
||||||
|
* Reinterpret a byte array as 32-bit words.
|
||||||
|
* ```ts
|
||||||
|
* u32(new Uint8Array(8));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function u32(arr) {
|
||||||
|
return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Zeroizes typed arrays in place. Warning: JS provides no guarantees.
|
||||||
|
* @param arrays - arrays to overwrite with zeros
|
||||||
|
* @example
|
||||||
|
* Zeroize sensitive buffers in place.
|
||||||
|
* ```ts
|
||||||
|
* clean(new Uint8Array([1, 2, 3]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function clean(...arrays) {
|
||||||
|
for (let i = 0; i < arrays.length; i++) {
|
||||||
|
arrays[i].fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a DataView for byte-level manipulation.
|
||||||
|
* @param arr - source typed array
|
||||||
|
* @returns DataView over the same buffer region.
|
||||||
|
* @example
|
||||||
|
* Create a DataView over an existing buffer.
|
||||||
|
* ```ts
|
||||||
|
* createView(new Uint8Array(4));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createView(arr) {
|
||||||
|
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Rotate-right operation for uint32 values.
|
||||||
|
* @param word - source word
|
||||||
|
* @param shift - shift amount in bits
|
||||||
|
* @returns Rotated word.
|
||||||
|
* @example
|
||||||
|
* Rotate a 32-bit word to the right.
|
||||||
|
* ```ts
|
||||||
|
* rotr(0x12345678, 8);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function rotr(word, shift) {
|
||||||
|
return (word << (32 - shift)) | (word >>> shift);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Rotate-left operation for uint32 values.
|
||||||
|
* @param word - source word
|
||||||
|
* @param shift - shift amount in bits
|
||||||
|
* @returns Rotated word.
|
||||||
|
* @example
|
||||||
|
* Rotate a 32-bit word to the left.
|
||||||
|
* ```ts
|
||||||
|
* rotl(0x12345678, 8);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function rotl(word, shift) {
|
||||||
|
return (word << shift) | ((word >>> (32 - shift)) >>> 0);
|
||||||
|
}
|
||||||
|
/** Whether the current platform is little-endian. */
|
||||||
|
export const isLE = /* @__PURE__ */ (() => new Uint8Array(new Uint32Array([0x11223344]).buffer)[0] === 0x44)();
|
||||||
|
/**
|
||||||
|
* Byte-swap operation for uint32 values.
|
||||||
|
* @param word - source word
|
||||||
|
* @returns Word with reversed byte order.
|
||||||
|
* @example
|
||||||
|
* Reverse the byte order of a 32-bit word.
|
||||||
|
* ```ts
|
||||||
|
* byteSwap(0x11223344);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function byteSwap(word) {
|
||||||
|
return (((word << 24) & 0xff000000) |
|
||||||
|
((word << 8) & 0xff0000) |
|
||||||
|
((word >>> 8) & 0xff00) |
|
||||||
|
((word >>> 24) & 0xff));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Conditionally byte-swaps one 32-bit word on big-endian platforms.
|
||||||
|
* @param n - source word
|
||||||
|
* @returns Original or byte-swapped word depending on platform endianness.
|
||||||
|
* @example
|
||||||
|
* Normalize a 32-bit word for host endianness.
|
||||||
|
* ```ts
|
||||||
|
* swap8IfBE(0x11223344);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const swap8IfBE = isLE
|
||||||
|
? (n) => n
|
||||||
|
: (n) => byteSwap(n) >>> 0;
|
||||||
|
/**
|
||||||
|
* Byte-swaps every word of a Uint32Array in place.
|
||||||
|
* @param arr - array to mutate
|
||||||
|
* @returns The same array after mutation; callers pass live state arrays here.
|
||||||
|
* @example
|
||||||
|
* Reverse the byte order of every word in place.
|
||||||
|
* ```ts
|
||||||
|
* byteSwap32(new Uint32Array([0x11223344]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function byteSwap32(arr) {
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
arr[i] = byteSwap(arr[i]);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Conditionally byte-swaps a Uint32Array on big-endian platforms.
|
||||||
|
* @param u - array to normalize for host endianness
|
||||||
|
* @returns Original or byte-swapped array depending on platform endianness.
|
||||||
|
* On big-endian runtimes this mutates `u` in place via `byteSwap32(...)`.
|
||||||
|
* @example
|
||||||
|
* Normalize a word array for host endianness.
|
||||||
|
* ```ts
|
||||||
|
* swap32IfBE(new Uint32Array([0x11223344]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const swap32IfBE = isLE
|
||||||
|
? (u) => u
|
||||||
|
: byteSwap32;
|
||||||
|
// Built-in hex conversion https://caniuse.com/mdn-javascript_builtins_uint8array_fromhex
|
||||||
|
const hasHexBuiltin = /* @__PURE__ */ (() =>
|
||||||
|
// @ts-ignore
|
||||||
|
typeof Uint8Array.from([]).toHex === 'function' && typeof Uint8Array.fromHex === 'function')();
|
||||||
|
// Array where index 0xf0 (240) is mapped to string 'f0'
|
||||||
|
const hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'));
|
||||||
|
/**
|
||||||
|
* Convert byte array to hex string.
|
||||||
|
* Uses the built-in function when available and assumes it matches the tested
|
||||||
|
* fallback semantics.
|
||||||
|
* @param bytes - bytes to encode
|
||||||
|
* @returns Lowercase hexadecimal string.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @example
|
||||||
|
* Convert bytes to lowercase hexadecimal.
|
||||||
|
* ```ts
|
||||||
|
* bytesToHex(Uint8Array.from([0xca, 0xfe, 0x01, 0x23])); // 'cafe0123'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function bytesToHex(bytes) {
|
||||||
|
abytes(bytes);
|
||||||
|
// @ts-ignore
|
||||||
|
if (hasHexBuiltin)
|
||||||
|
return bytes.toHex();
|
||||||
|
// pre-caching improves the speed 6x
|
||||||
|
let hex = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
hex += hexes[bytes[i]];
|
||||||
|
}
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
// We use optimized technique to convert hex string to byte array
|
||||||
|
const asciis = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 };
|
||||||
|
function asciiToBase16(ch) {
|
||||||
|
if (ch >= asciis._0 && ch <= asciis._9)
|
||||||
|
return ch - asciis._0; // '2' => 50-48
|
||||||
|
if (ch >= asciis.A && ch <= asciis.F)
|
||||||
|
return ch - (asciis.A - 10); // 'B' => 66-(65-10)
|
||||||
|
if (ch >= asciis.a && ch <= asciis.f)
|
||||||
|
return ch - (asciis.a - 10); // 'b' => 98-(97-10)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Convert hex string to byte array. Uses built-in function, when available.
|
||||||
|
* @param hex - hexadecimal string to decode
|
||||||
|
* @returns Decoded bytes.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @throws On wrong argument ranges or values. {@link RangeError}
|
||||||
|
* @example
|
||||||
|
* Decode lowercase hexadecimal into bytes.
|
||||||
|
* ```ts
|
||||||
|
* hexToBytes('cafe0123'); // Uint8Array.from([0xca, 0xfe, 0x01, 0x23])
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function hexToBytes(hex) {
|
||||||
|
if (typeof hex !== 'string')
|
||||||
|
throw new TypeError('hex string expected, got ' + typeof hex);
|
||||||
|
if (hasHexBuiltin) {
|
||||||
|
try {
|
||||||
|
return Uint8Array.fromHex(hex);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof SyntaxError)
|
||||||
|
throw new RangeError(error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hl = hex.length;
|
||||||
|
const al = hl / 2;
|
||||||
|
if (hl % 2)
|
||||||
|
throw new RangeError('hex string expected, got unpadded hex of length ' + hl);
|
||||||
|
const array = new Uint8Array(al);
|
||||||
|
for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {
|
||||||
|
const n1 = asciiToBase16(hex.charCodeAt(hi));
|
||||||
|
const n2 = asciiToBase16(hex.charCodeAt(hi + 1));
|
||||||
|
if (n1 === undefined || n2 === undefined) {
|
||||||
|
const char = hex[hi] + hex[hi + 1];
|
||||||
|
throw new RangeError('hex string expected, got non-hex character "' + char + '" at index ' + hi);
|
||||||
|
}
|
||||||
|
array[ai] = n1 * 16 + n2; // multiply first octet, e.g. 'a3' => 10*16+3 => 160 + 3 => 163
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* There is no setImmediate in browser and setTimeout is slow.
|
||||||
|
* This yields to the Promise/microtask scheduler queue, not to timers or the
|
||||||
|
* full macrotask event loop.
|
||||||
|
* @example
|
||||||
|
* Yield to the next scheduler tick.
|
||||||
|
* ```ts
|
||||||
|
* await nextTick();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const nextTick = async () => { };
|
||||||
|
/**
|
||||||
|
* Returns control to the Promise/microtask scheduler every `tick`
|
||||||
|
* milliseconds to avoid blocking long loops.
|
||||||
|
* @param iters - number of loop iterations to run
|
||||||
|
* @param tick - maximum time slice in milliseconds
|
||||||
|
* @param cb - callback executed on each iteration
|
||||||
|
* @example
|
||||||
|
* Run a loop that periodically yields back to the event loop.
|
||||||
|
* ```ts
|
||||||
|
* await asyncLoop(2, 0, () => {});
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function asyncLoop(iters, tick, cb) {
|
||||||
|
let ts = Date.now();
|
||||||
|
for (let i = 0; i < iters; i++) {
|
||||||
|
cb(i);
|
||||||
|
// Date.now() is not monotonic, so in case if clock goes backwards we return return control too
|
||||||
|
const diff = Date.now() - ts;
|
||||||
|
if (diff >= 0 && diff < tick)
|
||||||
|
continue;
|
||||||
|
await nextTick();
|
||||||
|
ts += diff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Converts string to bytes using UTF8 encoding.
|
||||||
|
* Built-in doesn't validate input to be string: we do the check.
|
||||||
|
* Non-ASCII details are delegated to the platform `TextEncoder`.
|
||||||
|
* @param str - string to encode
|
||||||
|
* @returns UTF-8 encoded bytes.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @example
|
||||||
|
* Encode a string as UTF-8 bytes.
|
||||||
|
* ```ts
|
||||||
|
* utf8ToBytes('abc'); // Uint8Array.from([97, 98, 99])
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function utf8ToBytes(str) {
|
||||||
|
if (typeof str !== 'string')
|
||||||
|
throw new TypeError('string expected');
|
||||||
|
return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Helper for KDFs: consumes Uint8Array or string.
|
||||||
|
* String inputs are UTF-8 encoded; byte-array inputs stay aliased to the caller buffer.
|
||||||
|
* @param data - user-provided KDF input
|
||||||
|
* @param errorTitle - label included in thrown errors
|
||||||
|
* @returns Byte representation of the input.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @example
|
||||||
|
* Normalize KDF input to bytes.
|
||||||
|
* ```ts
|
||||||
|
* kdfInputToBytes('password');
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function kdfInputToBytes(data, errorTitle = '') {
|
||||||
|
if (typeof data === 'string')
|
||||||
|
return utf8ToBytes(data);
|
||||||
|
return abytes(data, undefined, errorTitle);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Copies several Uint8Arrays into one.
|
||||||
|
* @param arrays - arrays to concatenate
|
||||||
|
* @returns Concatenated byte array.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @example
|
||||||
|
* Concatenate multiple byte arrays.
|
||||||
|
* ```ts
|
||||||
|
* concatBytes(new Uint8Array([1]), new Uint8Array([2]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function concatBytes(...arrays) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < arrays.length; i++) {
|
||||||
|
const a = arrays[i];
|
||||||
|
abytes(a);
|
||||||
|
sum += a.length;
|
||||||
|
}
|
||||||
|
const res = new Uint8Array(sum);
|
||||||
|
for (let i = 0, pad = 0; i < arrays.length; i++) {
|
||||||
|
const a = arrays[i];
|
||||||
|
res.set(a, pad);
|
||||||
|
pad += a.length;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Merges default options and passed options.
|
||||||
|
* @param defaults - base option object
|
||||||
|
* @param opts - user overrides
|
||||||
|
* @returns Merged option object. The merge mutates `defaults` in place.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @example
|
||||||
|
* Merge user overrides onto default options.
|
||||||
|
* ```ts
|
||||||
|
* checkOpts({ dkLen: 32 }, { asyncTick: 10 });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function checkOpts(defaults, opts) {
|
||||||
|
if (opts !== undefined && {}.toString.call(opts) !== '[object Object]')
|
||||||
|
throw new TypeError('options must be object or undefined');
|
||||||
|
const merged = Object.assign(defaults, opts);
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a callable hash function from a stateful class constructor.
|
||||||
|
* @param hashCons - hash constructor or factory
|
||||||
|
* @param info - optional metadata such as DER OID
|
||||||
|
* @returns Frozen callable hash wrapper with `.create()`.
|
||||||
|
* Wrapper construction eagerly calls `hashCons(undefined)` once to read
|
||||||
|
* `outputLen` / `blockLen`, so constructor side effects happen at module
|
||||||
|
* init time.
|
||||||
|
* @example
|
||||||
|
* Wrap a stateful hash constructor into a callable helper.
|
||||||
|
* ```ts
|
||||||
|
* import { createHasher } from '@noble/hashes/utils.js';
|
||||||
|
* import { sha256 } from '@noble/hashes/sha2.js';
|
||||||
|
* const wrapped = createHasher(sha256.create, { oid: sha256.oid });
|
||||||
|
* wrapped(new Uint8Array([1]));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createHasher(hashCons, info = {}) {
|
||||||
|
const hashC = (msg, opts) => hashCons(opts)
|
||||||
|
.update(msg)
|
||||||
|
.digest();
|
||||||
|
const tmp = hashCons(undefined);
|
||||||
|
hashC.outputLen = tmp.outputLen;
|
||||||
|
hashC.blockLen = tmp.blockLen;
|
||||||
|
hashC.canXOF = tmp.canXOF;
|
||||||
|
hashC.create = (opts) => hashCons(opts);
|
||||||
|
Object.assign(hashC, info);
|
||||||
|
return Object.freeze(hashC);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Cryptographically secure PRNG backed by `crypto.getRandomValues`.
|
||||||
|
* @param bytesLength - number of random bytes to generate
|
||||||
|
* @returns Random bytes.
|
||||||
|
* The platform `getRandomValues()` implementation still defines any
|
||||||
|
* single-call length cap, and this helper rejects oversize requests
|
||||||
|
* with a stable library `RangeError` instead of host-specific errors.
|
||||||
|
* @throws On wrong argument types. {@link TypeError}
|
||||||
|
* @throws On wrong argument ranges or values. {@link RangeError}
|
||||||
|
* @throws If the current runtime does not provide `crypto.getRandomValues`. {@link Error}
|
||||||
|
* @example
|
||||||
|
* Generate a fresh random key or nonce.
|
||||||
|
* ```ts
|
||||||
|
* const key = randomBytes(16);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function randomBytes(bytesLength = 32) {
|
||||||
|
// Match the repo's other length-taking helpers instead of relying on Uint8Array coercion.
|
||||||
|
anumber(bytesLength, 'bytesLength');
|
||||||
|
const cr = typeof globalThis === 'object' ? globalThis.crypto : null;
|
||||||
|
if (typeof cr?.getRandomValues !== 'function')
|
||||||
|
throw new Error('crypto.getRandomValues must be defined');
|
||||||
|
// Web Cryptography API Level 2 §10.1.1:
|
||||||
|
// if `byteLength > 65536`, throw `QuotaExceededError`.
|
||||||
|
// Keep the guard explicit so callers can see the quota in code
|
||||||
|
// instead of discovering it by reading the spec or host errors.
|
||||||
|
// This wrapper surfaces the same quota as a stable library RangeError.
|
||||||
|
if (bytesLength > 65536)
|
||||||
|
throw new RangeError(`"bytesLength" expected <= 65536, got ${bytesLength}`);
|
||||||
|
return cr.getRandomValues(new Uint8Array(bytesLength));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates OID metadata for NIST hashes with prefix `06 09 60 86 48 01 65 03 04 02`.
|
||||||
|
* @param suffix - final OID byte for the selected hash.
|
||||||
|
* The helper accepts any byte even though only the documented NIST hash
|
||||||
|
* suffixes are meaningful downstream.
|
||||||
|
* @returns Object containing the DER-encoded OID.
|
||||||
|
* @example
|
||||||
|
* Build OID metadata for a NIST hash.
|
||||||
|
* ```ts
|
||||||
|
* oidNist(0x01);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const oidNist = (suffix) => ({
|
||||||
|
// Current NIST hashAlgs suffixes used here fit in one DER subidentifier octet.
|
||||||
|
// Larger suffix values would need base-128 OID encoding and a different length byte.
|
||||||
|
oid: Uint8Array.from([0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, suffix]),
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=utils.js.map
|
||||||