Merge pull request #67 from Coldcard/v4

Firmware V4
This commit is contained in:
Peter 2021-03-17 13:15:24 -04:00 committed by GitHub
commit 94bb8a3b2b
169 changed files with 3716 additions and 3091 deletions

9
.gitmodules vendored
View File

@ -2,15 +2,12 @@
path = external/micropython
url = https://github.com/Coldcard/micropython.git
branch = master
[submodule "external/modcryptocurrency"]
path = external/modcryptocurrency
url = https://github.com/Coldcard/modcryptocurrency.git
[submodule "external/crypto"]
path = external/crypto
url = https://github.com/peter-conalgo/trezor-crypto.git
[submodule "external/ckcc-protocol"]
path = external/ckcc-protocol
url = https://github.com/Coldcard/ckcc-protocol.git
[submodule "external/mpy-qr"]
path = external/mpy-qr
url = https://github.com/coinkite/mpy-qr.git
[submodule "external/libngu"]
path = external/libngu
url = https://github.com/switck/libngu.git

674
COPYING
View File

@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is 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. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
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.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
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 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. Use with the GNU Affero General Public License.
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 Affero 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 special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU 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 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 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 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 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
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 GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

16
LICENSE
View File

@ -1,16 +0,0 @@
(c) Copyright 2017-2020 by Coinkite Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License
in the file COPYING. If not, see <http://www.gnu.org/licenses/>.

1
LICENSE Symbolic link
View File

@ -0,0 +1 @@
COPYING-CC

View File

@ -12,6 +12,23 @@ with the latest updates and security alerts.
![coldcard picture front](https://coldcardwallet.com/static/images/coldcard-front.png)
![coldcard picture back](https://coldcardwallet.com/static/images/coldcard-back.png)
## Reproducable Builds
To have confidence this source code tree is the same as the binary on your device,
you can rebuild it from source and get **exactly the same bytes**. This process
has been automated using Docker. Steps are as follows:
1. Install Docker and start it.
2. Install [make (GNUMake)](https://www.gnu.org/software/make/) if you don't already have it.
3. Checkout the code, and start the process.
git clone https://github.com/Coldcard/firmware.git
cd firmware/stm32
make repro
4. At the end of the process a clear confirmation message is shown, or the differences.
5. Build products can be found `firmware/stm32/built`.
## Check-out and Setup
Do a checkout, recursively to get all the submodules:

View File

@ -1,15 +1,4 @@
# NOTE: much of this is implied by ckcc-protocol/setup
# may need "brew install hidapi" before this?
hidapi>=0.7.99.post21
# only items needed for signit.py
click>=6.7
# required by signit.py and link-layer encryption in client.py
ecdsa>=0.13
# so pydfu.py can run?
pyusb
# for link-layer encryption
pyaes

View File

@ -94,6 +94,10 @@ def show_version(fname):
# just dump the version number in a form that makes for good filenames
data = open(fname, 'rb').read()
if data[0:5] == b'DfuSe':
# Got DFU file, pulling out raw binary.
(_, _, data),*_ = dfu_parse(open(fname, 'rb'))
hdr = data[FW_HEADER_OFFSET:FW_HEADER_OFFSET+FW_HEADER_SIZE ]
hdr = header(**dict(zip(FWH_PY_VALUES.split(), struct.unpack(FWH_PY_FORMAT, hdr))))
@ -105,13 +109,82 @@ def show_version(fname):
print('{built}-v{ver}'.format(built=built, ver=ver))
def dfu_parse(fd):
# do just a little parsing of DFU headers, to find start/length of main binary
# - not trying to support anything but what ../stm32/Makefile will generate
# - see external/micropython/tools/pydfu.py for details
# - works sequentially only
import struct
from collections import namedtuple
fd.seek(0)
def consume(xfd, tname, fmt, names):
# Parses the struct defined by `fmt` from `data`, stores the parsed fields
# into a named tuple using `names`. Returns the named tuple.
size = struct.calcsize(fmt)
here = xfd.read(size)
ty = namedtuple(tname, names.split())
values = struct.unpack(fmt, here)
return ty(*values)
dfu_prefix = consume(fd, 'DFU', '<5sBIB', 'signature version size targets')
#print('dfu: ' + repr(dfu_prefix))
assert dfu_prefix.signature == b'DfuSe', "Not a DFU file (bad magic)"
for idx in range(dfu_prefix.targets):
prefix = consume(fd, 'Target', '<6sBI255s2I',
'signature altsetting named name size elements')
#print("target%d: %r" % (idx, prefix))
for ei in range(prefix.elements):
# Decode target prefix
# < little endian
# I uint32_t element address
# I uint32_t element size
elem = consume(fd, 'Element', '<2I', 'addr size')
#print("target%d: %r" % (ei, elem))
yield fd.tell(), elem.size, fd.read(elem.size)
@main.command('split')
@click.argument('dfu', metavar='202....-coldcard.dfu')
@click.argument('firmware', metavar='FIRMWARE.bin')
@click.argument('bootrom', metavar='BOOTROM.bin')
def split_dfu(dfu, firmware, bootrom):
"Pull out sections from DFU file for verification purposes"
with open(dfu, 'rb') as fd:
for n, (off, ln, data) in enumerate(dfu_parse(fd)):
if n == 0:
target = firmware
name = 'Firmware'
elif n == 1:
target = bootrom
name = 'Bootrom'
else:
raise ValueError(n)
# keep this printout so others can check our copy is faithful
print(f'start {off} for {ln} bytes: {name} => {target}')
open(target, 'wb').write(data)
@main.command('check')
@click.argument('fname', default='firmware-signed.bin')
def readback(fname):
"Verify pubkey and signature used in binary file"
data = open(fname, 'rb').read()
assert data[0:5] != b'DfuSe', "got DFU, expect raw binary"
if data[0:5] == b'DfuSe':
click.secho("Got DFU file, pulling out raw binary.", fg='red')
(_, _, data),*_ = dfu_parse(open(fname, 'rb'))
hdr = data[FW_HEADER_OFFSET:FW_HEADER_OFFSET+FW_HEADER_SIZE ]
@ -187,6 +260,14 @@ def doit(keydir, outfn=None, build_dir='l-port/build-COLDCARD', high_water=False
"Add signature into binary file before it becomes a DFU file."
assert len(version) < 8, "Version string limited to 8 bytes, got: %r" % version
# load key
try:
sk = SigningKey.from_pem(open(f"{keydir}/{pubkey_num:02d}.pem").read())
except FileNotFoundError:
click.secho(f"You don't have that key ({pubkey_num}), so using key zero instead!", fg='red')
pubkey_num = 0
sk = SigningKey.from_pem(open(f"{keydir}/{pubkey_num:02d}.pem").read())
if resign_file:
whole = resign_file.read()
@ -249,9 +330,6 @@ def doit(keydir, outfn=None, build_dir='l-port/build-COLDCARD', high_water=False
print("Hdr: %s" % repr(hdr))
print('Hash: %s' % b2a_hex(fw_hash).decode('ascii'))
# load key
sk = SigningKey.from_pem(open(f"{keydir}/{pubkey_num:02d}.pem").read())
from ecdsa.util import sigencode_string
sig = sk.sign_digest(fw_hash, sigencode=sigencode_string)

View File

@ -34,20 +34,22 @@ process and that the data is in fact encrypted. Any 7z tool that
supports AES256-SHA256 encryption should be able to read the files
we make. Take the 12 words and put them together with a single space
between each word (all lowercase). The decoded archive will contain
a single file, `ckcc-backup.txt`, which is a simple text file and
easy to read.
a single file, which is a simple text file and
easy to read. Before version 4.0.0, this text file was always
called `ckcc-backup.txt`, but the filename is now picked randomly.
## Limitations
- The archive file names are not encrypted. You can see `ckcc-backup.txt` in
the hex dump of the encrypted file, encoded as `utf-16-le` bytes.
- The archive file names are not encrypted. You can see there is a single
text file `word(number).txt` in the encrypted file without decrypting it.
- The device PIN code is not preserved during backup.
- We produce standards-compliant files, but do not support reading any
file except the ones produced by Coldcard.
- Do not attempt to edit the file and restore it onto a Coldcard.
- You cannot construct a file for the Coldcard to read because we implement only
enough to support reading files that we know that we've produced.
- There is no plausible deniability here: the 7z file is clearly a Coldcard backup file.
- There is limited plausible deniability here: if you are forced to decrypt
the file, it is clearly a Coldcard backup file.
## Example File
@ -109,6 +111,14 @@ If you are playing along at home, the passphrase for the above file is:
You can grab the [example file here](backup.7z) and test it yourself, or use
a real Coldcard to make your own.
## Internal Filenames
The internal filename in these examples is `ckcc-backup.txt`, but
starting in version 4.0.0 of the Coldcard firwmare, your backup
files will each have a different random filename inside. Use
`7z l backup.7z` to view the actual filename inside the 7z file.
### Archive Contents
```

2
external/c-modules/README.md vendored Normal file
View File

@ -0,0 +1,2 @@
This link forest allows micropython's makefile to find the C-language modules
we are using today.

View File

@ -0,0 +1,7 @@
# AES-256 for CTR mode (only)
- from <https://github.com/Ko-/aes-armcortexm> under CC0-1.0
- ASM for ARM (only)
- very thin interface

View File

@ -0,0 +1,12 @@
#pragma once
#include <stdint.h>
typedef struct param {
uint8_t nonce[12];
uint8_t ctr[4];
uint8_t rk[15*16];
} param;
extern void AES_256_keyschedule(const uint8_t *key, uint8_t *);
extern void AES_256_encrypt_ctr(param const *, const uint8_t *, uint8_t *, uint32_t);

1308
external/c-modules/aes256ctr/aes_256_ctr.s vendored Normal file

File diff suppressed because it is too large Load Diff

110
external/c-modules/aes256ctr/example.c vendored Normal file
View File

@ -0,0 +1,110 @@
// from https://raw.githubusercontent.com/Ko-/aes-armcortexm/public/aes256ctr/aes_256_ctr.c
// taken Feb 25/2021
// REFERENCE ONLY -- not used.
#include "../common/stm32wrapper.h"
#include <stdio.h>
#include <stdint.h>
#include <string.h>
typedef struct param {
uint32_t ctr;
uint8_t nonce[12];
uint8_t rk[15*16];
} param;
extern void AES_256_keyschedule(const uint8_t *, uint8_t *);
extern void AES_256_encrypt_ctr(param const *, const uint8_t *, uint8_t *, uint32_t);
#define AES_256_decrypt_ctr AES_256_encrypt_ctr
int main(void)
{
clock_setup();
gpio_setup();
usart_setup(115200);
// plainly reading from CYCCNT is more efficient than using the
// dwt_read_cycle_counter() interface offered by libopencm3,
// as this adds extra overhead because of the function call
SCS_DEMCR |= SCS_DEMCR_TRCENA;
DWT_CYCCNT = 0;
DWT_CTRL |= DWT_CTRL_CYCCNTENA;
const uint32_t LEN = 256*16;
const uint32_t LEN_ROUNDED = ((LEN+15)/16)*16;
const uint8_t nonce[12] = {1,2,3,1,2,4,1,2,5,1,2,6};
const uint8_t key[32] = {4,5,6,7,4,5,6,8,4,5,6,9,4,5,6,10,4,5,6,11,4,5,6,12,4,5,6,13,4,5,6,14};
uint8_t in[LEN];
uint8_t out[LEN_ROUNDED];
unsigned int i;
for(i=0;i<LEN;++i)
in[i] = i%256;
char buffer[36];
param p;
p.ctr = 0;
memcpy(p.nonce, nonce, 12);
memcpy(p.rk, key, 32);
unsigned int oldcount = DWT_CYCCNT;
AES_256_keyschedule(key, p.rk+32);
unsigned int cyclecount = DWT_CYCCNT-oldcount;
/*
// Print all round keys
unsigned int j;
for(i=0;i<15*4;++i) {
sprintf(buffer, "rk[%2d]: ", i);
for(j=0;j<4;++j)
sprintf(buffer+2*j+8, "%02x", p.rk[i*4+j]);
send_USART_str(buffer);
}
*/
sprintf(buffer, "cyc: %d", cyclecount);
send_USART_str(buffer);
oldcount = DWT_CYCCNT;
AES_256_encrypt_ctr(&p, in, out, LEN);
cyclecount = DWT_CYCCNT-oldcount;
sprintf(buffer, "cyc: %d", cyclecount);
send_USART_str(buffer);
/*
// Print ciphertext
sprintf(buffer, "out: ");
send_USART_str(buffer);
for(i=0;i<LEN;++i) {
sprintf(buffer+((2*i)%32), "%02x", out[i]);
if(i%16 == 15)
send_USART_str(buffer);
}
if(LEN%16 > 0)
send_USART_str(buffer);
*/
/*
// Perform decryption
p.ctr = 0;
AES_256_decrypt_ctr(&p, out, in, LEN);
// Print plaintext
sprintf(buffer, "in: ");
send_USART_str(buffer);
for(i=0;i<LEN;++i) {
sprintf(buffer+((2*i)%32), "%02x", in[i]);
if(i%16 == 15)
send_USART_str(buffer);
}
if(LEN%16 > 0)
send_USART_str(buffer);
*/
while (1);
return 0;
}

View File

@ -0,0 +1,11 @@
#
# Pure ASM version of AES-256 for CTR mode only
#
ifeq ($(BOARD), COLDCARD)
SRC_USERMOD += $(USERMOD_DIR)/aes_256_ctr.o
SRC_USERMOD += $(USERMOD_DIR)/module.c
endif

187
external/c-modules/aes256ctr/module.c vendored Normal file
View File

@ -0,0 +1,187 @@
//
// module top-level
//
#include "py/obj.h"
#include "py/runtime.h"
#include "py/builtin.h"
#include "aes_256_ctr.h"
#if MICROPY_ENABLE_DYNRUNTIME
#error "Static Only"
#endif
// AES block size
#define BLKSIZE 16
typedef struct {
mp_obj_base_t base;
uint8_t runt[BLKSIZE];
int runt_len;
param p;
} mp_obj_AES256CTR_t;
STATIC const mp_obj_type_t s_AES256CTR_type;
STATIC mp_obj_t s_AES256CTR_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
// args: key, nonce
mp_arg_check_num(n_args, n_kw, 1, 2, false);
mp_buffer_info_t key;
mp_get_buffer_raise(args[0], &key, MP_BUFFER_READ);
if(key.len != 32) {
// only AES-256 here
mp_raise_ValueError(NULL);
}
mp_obj_AES256CTR_t *o = m_new_obj_with_finaliser(mp_obj_AES256CTR_t);
o->base.type = type;
// state setup, and key schedule
memset(&o->p, 0, sizeof(param));
o->runt_len = 0;
if(n_args == 2) {
mp_buffer_info_t n_in;
mp_get_buffer_raise(args[1], &n_in, MP_BUFFER_READ);
if(n_in.len > 16) {
mp_raise_ValueError(NULL);
}
// can be 12 bytes of nonce + 4 bytes ctr, or just all 16 bytes of state
memcpy(o->p.nonce, n_in.buf, n_in.len);
}
// key setup
memcpy(o->p.rk, key.buf, 32);
AES_256_keyschedule(key.buf, o->p.rk+32);
return o;
}
static bool inline
is_unaligned(const void *p)
{
return !!(((uint32_t)p) & 0x3);
}
STATIC mp_obj_t s_AES256CTR_cipher(mp_obj_t self_in, mp_obj_t buf_in)
{
mp_obj_AES256CTR_t *self = MP_OBJ_TO_PTR(self_in);
mp_buffer_info_t buf;
mp_get_buffer_raise(buf_in, &buf, MP_BUFFER_READ);
int len = buf.len, in_len = buf.len;
const uint8_t *inp = buf.buf;
uint8_t *rv = m_malloc(in_len);
uint8_t *outp = rv;
if(self->runt_len) {
// we've already encrypted (w/ zero bytes) for this part
uint8_t *ch = &self->runt[BLKSIZE - self->runt_len];
while(self->runt_len && in_len) {
*(outp++) = *(inp++) ^ *(ch++);
self->runt_len --;
in_len --;
}
}
bool nogood = (is_unaligned(inp) || is_unaligned(outp));
while(in_len) {
if(in_len >= BLKSIZE) {
if(nogood) {
uint8_t blk[BLKSIZE];
// align data so ASM can work
memcpy(blk, inp, BLKSIZE);
AES_256_encrypt_ctr(&self->p, blk, blk, BLKSIZE);
memcpy(outp, blk, BLKSIZE);
} else {
// rare? but faster case
AES_256_encrypt_ctr(&self->p, inp, outp, BLKSIZE);
}
outp += BLKSIZE;
inp += BLKSIZE;
in_len -= BLKSIZE;
} else {
uint8_t blk[BLKSIZE] = {};
memcpy(blk, inp, in_len);
AES_256_encrypt_ctr(&self->p, blk, self->runt, BLKSIZE);
memcpy(outp, self->runt, in_len);
self->runt_len = BLKSIZE - in_len;
in_len = 0;
}
// inc counter (big endian, assumes nonce != ~0)
uint8_t *ctr = &self->p.ctr[3];
while(1) {
ctr[0] += 1;
if(ctr[0]) break;
ctr--;
}
}
return mp_obj_new_bytearray_by_ref(len, rv);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(s_AES256CTR_cipher_obj, s_AES256CTR_cipher);
STATIC mp_obj_t s_AES256CTR_copy(mp_obj_t self_in) {
mp_obj_AES256CTR_t *self = MP_OBJ_TO_PTR(self_in);
mp_obj_AES256CTR_t *rv = m_new_obj_with_finaliser(mp_obj_AES256CTR_t);
*rv = *self;
rv->base.type = &s_AES256CTR_type;
return rv;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(s_AES256CTR_copy_obj, s_AES256CTR_copy);
STATIC mp_obj_t s_AES256CTR_blank(mp_obj_t self_in) {
mp_obj_AES256CTR_t *self = MP_OBJ_TO_PTR(self_in);
// cf_aes_finish is just this anyway
memset(self, 0, sizeof(mp_obj_AES256CTR_t));
self->base.type = &s_AES256CTR_type;
return self_in;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(s_AES256CTR_blank_obj, s_AES256CTR_blank);
STATIC const mp_rom_map_elem_t s_AES256CTR_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_cipher), MP_ROM_PTR(&s_AES256CTR_cipher_obj) },
{ MP_ROM_QSTR(MP_QSTR_blank), MP_ROM_PTR(&s_AES256CTR_blank_obj) },
{ MP_ROM_QSTR(MP_QSTR_blank), MP_ROM_PTR(&s_AES256CTR_blank_obj) },
{ MP_ROM_QSTR(MP_QSTR_copy), MP_ROM_PTR(&s_AES256CTR_copy_obj) },
{ MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&s_AES256CTR_blank_obj) },
};
STATIC MP_DEFINE_CONST_DICT(s_AES256CTR_locals_dict, s_AES256CTR_locals_dict_table);
STATIC const mp_obj_type_t s_AES256CTR_type = {
{ &mp_type_type },
.name = MP_QSTR_AES256CTR,
.make_new = s_AES256CTR_make_new,
.locals_dict = (void *)&s_AES256CTR_locals_dict,
};
STATIC const mp_rom_map_elem_t mp_module_aes256ctr_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_aes256ctr) },
{ MP_ROM_QSTR(MP_QSTR_new), MP_ROM_PTR(&s_AES256CTR_type) },
};
STATIC MP_DEFINE_CONST_DICT(mp_module_aes256ctr_globals, mp_module_aes256ctr_globals_table);
const mp_obj_module_t mp_module_aes256ctr = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t *)&mp_module_aes256ctr_globals,
};
MP_REGISTER_MODULE(MP_QSTR_aes256ctr, mp_module_aes256ctr, 1);

1
external/c-modules/libngu vendored Symbolic link
View File

@ -0,0 +1 @@
../libngu/ngu

1
external/c-modules/mpy-qr vendored Symbolic link
View File

@ -0,0 +1 @@
../mpy-qr

@ -1 +1 @@
Subproject commit f3033b9dce8619c3ad89bdd840f5c91156d9598c
Subproject commit 56db7699271300c9a32e8645257c8f2177c56bd7

1
external/crypto vendored

@ -1 +0,0 @@
Subproject commit f83020c73dbb3d033b344345c99792d430d644c6

1
external/libngu vendored Submodule

@ -0,0 +1 @@
Subproject commit 74b373c2b7c92e6e903be22da773bad3f0daa09b

@ -1 +1 @@
Subproject commit db4e297ea1f40d2105cea1e2592e46fbda918dbc
Subproject commit 4db3518e4060b333a7320602c04885d1e6503618

@ -1 +0,0 @@
Subproject commit aba6ba441b0d51b255e680e1988a4d240155f849

2
external/mpy-qr vendored

@ -1 +1 @@
Subproject commit 198042652fa5720efeed09d20aee436ddbf35ece
Subproject commit 3ccf19ca142e9059904f0c8e53b6baeccb9c6b79

View File

@ -1,3 +1,39 @@
## 4.0.0b4 BETA - March 17, 2021
**BETA RELEASE** — (release candidate for v4.0.0)
- Major internal changes.
- now using [Bitcoin Core's "libsecp256k1"](https://github.com/bitcoin-core/secp256k1)
for all EC crypto operations
- super fast pure-assembly AES256-CTR code makes USB communications faster
- newly optimized SHA256 and SHA256(SHA256) code
- all crypto and BIP39 related code replaced
- huge thanks to [@switck](https://twitter.com/switck) for the new library!
- Enhancement: During seed phrase import, after 23 words provided, Coldcard will
calculate the correct checksum and show the valid choices for the last word (there
will be 8 typically). This means you can pick seed words by drawing from a hat.
- New feature: Secure Device Cloning. Using a MicroSD card, copy your Coldcard's secrets
and settings to a blank Coldcard. Very quick and easy, uses public key encryption
(Diffie-Hellman key exchange) and AES-256-CBC for the transfer.
- Bugfix: CSV of addresses explorer export via Address Explorer, when account number
was used, did not reflect the (non-zero) account number.
- Enhancement: Reproducible builds! Checkout code, "cd stm32; make repro" should do it all.
- Enhancement: Paper wallet feature restored as it was previously. Same cautions apply.
- Enhancement: Inside encrypted backup files (7z), the cleartext filename is no longer
fixed as `ckcc-backup.txt`. Instead it's a random word and number. Improves plausible
deniability when backup files discovered.
- Enhancement: Show a progress bar during slow parts of the login process.
- Enhancement: Long menus, like the seed-word picking system, now wrap around from top/bottom, so
you can get to Z by going up from A.
- Limitation: Mk2 (older hardware, with less memory) may struggle with some of the new
features, but can still run this firmware release... so you can clone it to your new Mk3!
- HSM/CKBunker mode changes:
- IMPORTANT: users with passwords will have to be reconstructed as hash algo has changed
- when unlocking HSM mode from "boot to HSM mode" (using secret PIN immediately after bootup)
the HSM policy is no longer removed automatically.
- time limit to escape "boot to HSM" mode has doubled from 30 seconds to 1 minute.
- Remaining GPL code has been removed, so license is now MIT+CC on everything.
## 3.2.2 - Jan 14, 2021
- Major Address Explorer enhancements! Thanks go to [@switck](https://twitter.com/switck)
@ -10,7 +46,7 @@
- Export of addresses now named "addresses.csv" not ".txt"
- Bugfix: Disable a few more path derivation checks for "Skip Checks" for
multisig compatibility. Handles error shown when working
with previously-imported Spectre multisig wallets (ie. `multisig.py: 891`).
with previously-imported Specter multisig wallets (ie. `multisig.py: 891`).
- Bugfix: Generic wallet export (JSON) name for BIP49 wallets changed
from "p2wpkh-p2sh" to "p2sh-p2wpkh". Thanks [@craigraw](https://twitter.com/craigraw)

View File

@ -1,7 +1,9 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
79b5d751711e66afb997063e8ab36310d3ad26786084c5379cd02afaa6a7e494 ChangeLog.md
6e376290cd6b0ccc13c8f90292f3d75dafb7a14df77d6d2711cb776cdcf14668 ChangeLog.md
6bc3b25ec06420b974adc9d6eb8c4d191a18d0e3b33669899b271f61224ea4a8 2021-03-17T1310-v4.0.0b4-coldcard.dfu
0066bd8162146261efc47af99d4eec848e519dbd611835c87e8a0a0e6792f127 2021-03-15T1341-v4.0.0b3-coldcard.dfu
3097fa3c173247637aa27376036e384940adeb67ce727c9795471f46deaa5210 2021-01-14T1617-v3.2.2-coldcard.dfu
9e4aeee48d4399a761fec5d4c65cb2495ef5bc0b46995c085d63a65cf67362cb 2021-01-07T1439-v3.2.1-coldcard.dfu
ac756e8f90d7cdc706fb24a1358585780c0105329e95d0095d91d48579caa5a1 2020-08-06T1722-v3.1.9-coldcard.dfu
@ -17,12 +19,12 @@ d655c51f0757c84e293daf42ee88eba0bcd66cefb148cb4c18097c323547c874 2020-02-27T135
bea27f263b524a66b3ed0a58c16805e98be0d7c3db20c2f7aab3238f2c6a6995 2019-12-19T1623-v3.0.6-coldcard.dfu
-----BEGIN PGP SIGNATURE-----
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmAAcd4ACgkQo6MbrVoq
WxD/zgf/aeuQSIdYRM3UGfF6tLYHcIa/BhsiZX0672cizMtLD1BR1end1+BAa9zE
QAz955L8Wpi6BF2X2L/sYSgES/MXB/hmBukUAOwblzJjYJO2+8qyLe7s+oty+QS6
VLUqrrxNoWUq8bVQuS3bi+fIvL2US86IOHLRuCuznbPUx0fnu0V8wQ/usWNrwS+J
6S1M0Gl1iUIZGMfO9MtHUf37vguQMp8TGqlOKrbEFhRfatI4sumCRTNPuyPgmVDf
f4sKd7J7zzIoAn79y+4D/HBqVzEpZ+mMawJeXwms2s0lnGjBpWjyDIw7Wsn1NijO
OQ3o2QxMpsDr3gUiqwKI97kIGT/Hew==
=BvHP
iQEzBAEBCAAdFiEERYl3mt/BTzMnU06oo6MbrVoqWxAFAmBR/8sACgkQo6MbrVoq
WxDmcQf/aNjvHGHH3lGrjVDwKWyd25rW16b0Tf5Sd3K/wYDGq2XRmhk8yzizAYwb
3+j2ENzDo9Fj/Hw+U13PDQR8mmCtCYq2S/tRSJPQgTbiGUowhsmvyPHrO2Kl63/u
0S3FUERYILr8yi9N1oV/0LXlNepd18bzi0hcoHNJc6A/VkAr12Ifm29i3xnY+eU1
Hwi1mKTr+gdE7/CMTpwysOyPoONdJzol3R43yecvwnDnwVbtROFWQSJjYVV+YfsE
1lde0dkUdehrSgiiiK4KC6pAKJNqbo7IW4u6WoBkPYveuGv27Ix9dcsqZs2mXncB
KUNa+LKctbjGGBq4fVV8M1A5ZabRzQ==
=7jD6
-----END PGP SIGNATURE-----

View File

@ -11,10 +11,10 @@ dfu:
# Find any python files newer than the last firmware DFU file, and copy
# them onto the flash filesystem of the device. They will replace the
# frozen versions of the same name.
NEWER = $(shell find . -name \*.py -newer ../stm32/firmware-signed.dfu)
up:
cp $(NEWER) /Volumes/COLDCARD/lib
#
#NEWER = $(shell find . -name \*.py -newer ../stm32/firmware-signed.dfu)
#up:
# cp $(NEWER) /Volumes/COLDCARD/lib
# Remove old junk code, restore use of frozen modules.
clean:

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# actions.py
#
@ -9,10 +8,12 @@ import ckcc, pyb, version
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_poll_once, ux_aborted
from ux import ux_enter_number
from utils import imported, pretty_short_delay, problem_file_line
from main import settings
import uasyncio
from uasyncio import sleep_ms
from files import CardSlot, CardMissingError
from utils import xfp2str
from nvstore import settings
from pincodes import pa
async def start_selftest(*args):
@ -74,7 +75,6 @@ Press OK to accept terms and continue.""", escape='7')
async def view_ident(*a):
# show the XPUB, and other ident on screen
from main import settings, pa
import callgate, stash
tpl = '''\
@ -111,12 +111,11 @@ Extended Master Key:
await ux_show_story(msg)
async def show_settings_space(*a):
from main import settings
await ux_show_story('Settings storage space in use:\n\n %d%%' % int(settings.capacity * 100))
async def maybe_dev_menu(*a):
from main import is_devmode
from version import is_devmode
if not is_devmode:
ok = await ux_confirm('Developer features could be used to weaken security or release key material.\n\nDo not proceed unless you know what you are doing and why.')
@ -169,8 +168,6 @@ Keep tmp files and other junk out!""")
async def dev_enable_protocol(*a):
# Turn off disk emulation. Keep VCP enabled, since they are still devs.
from main import loop
cur = pyb.usb_mode()
if cur and 'HID' in cur:
await ux_show_story('Coldcard USB protocol is already enabled (HID mode)')
@ -179,9 +176,12 @@ async def dev_enable_protocol(*a):
# might need to reset stuff?
from usb import enable_usb
# reset / re-enable
# reset and re-enable
pyb.usb_mode(None)
enable_usb(loop, True)
enable_usb()
# enable REPL
ckcc.vcp_enabled(True)
await ux_show_story('Back to normal USB mode.')
@ -201,7 +201,8 @@ async def microsd_upgrade(*a):
with CardSlot() as card:
with open(fn, 'rb') as fp:
from main import sf, dis
from sflash import SF
from glob import dis
from files import dfu_parse
from utils import check_firmware_hdr
@ -246,14 +247,14 @@ async def microsd_upgrade(*a):
if pos % 4096 == 0:
# erase here
sf.sector_erase(pos)
while sf.is_busy():
SF.sector_erase(pos)
while SF.is_busy():
await sleep_ms(10)
sf.write(pos, buf)
SF.write(pos, buf)
# full page write: 0.6 to 3ms
while sf.is_busy():
while SF.is_busy():
await sleep_ms(1)
pos += here
@ -323,7 +324,7 @@ Press 6 to prove you read to the end of this message.''', title='WARNING', escap
if pin is None: return
# A new pin is to be set!
from main import pa, dis, settings, loop
from glob import dis
dis.fullscreen("Saving...")
try:
@ -348,7 +349,7 @@ Press 6 to prove you read to the end of this message.''', title='WARNING', escap
# Allow USB protocol, now that we are auth'ed
from usb import enable_usb
enable_usb(loop, False)
enable_usb()
from menu import MenuSystem
from flow import EmptyWallet
@ -357,7 +358,7 @@ Press 6 to prove you read to the end of this message.''', title='WARNING', escap
async def login_countdown(minutes):
# show a countdown, which may need to
# run for multiple **days**
from main import dis
from glob import dis
from display import FontSmall, FontLarge
sec = minutes * 60
@ -385,7 +386,6 @@ async def block_until_login(rnd_keypad):
# Force user to enter a valid PIN.
#
from login import LoginUX
from main import pa, loop, settings
from ux import AbortInteraction
while not pa.is_successful():
@ -400,7 +400,7 @@ async def block_until_login(rnd_keypad):
async def show_nickname(nick):
# Show a nickname for this coldcard (as a personalization)
# - no keys here, just show it until they press anything
from main import dis
from glob import dis
from display import FontLarge, FontTiny, FontSmall
from ux import ux_wait_keyup
@ -566,7 +566,6 @@ async def clear_seed(*a):
# Erase the seed words, and private key from this wallet!
# This is super dangerous for the customer's money.
import seed
from main import pa
if pa.has_duress_pin():
await ux_show_story('''Please empty the duress wallet, and clear the duress PIN before clearing main seed.''')
@ -587,14 +586,14 @@ consequences.''', escape='4')
# NOT REACHED -- reset happens
async def view_seed_words(*a):
import stash, tcc
import stash, bip39
if not await ux_confirm('''The next screen will show the seed words (and if defined, your BIP39 passphrase).\n\nAnyone with knowledge of those words can control all funds in this wallet.''' ):
return
with stash.SensitiveValues() as sv:
if sv.mode == 'words':
words = tcc.bip39.from_data(sv.raw).split(' ')
words = bip39.b2a_words(sv.raw).split(' ')
msg = 'Seed words (%d):\n' % len(words)
msg += '\n'.join('%2d: %s' % (i+1, w) for i,w in enumerate(words))
@ -621,8 +620,8 @@ async def view_seed_words(*a):
async def start_login_sequence():
# Boot up login sequence here.
#
from main import pa, settings, dis, loop, numpad
from ux import idle_logout
from glob import dis
if pa.is_blank():
# Blank devices, with no PIN set all, can continue w/o login
@ -661,8 +660,9 @@ async def start_login_sequence():
await block_until_login(rnd_keypad)
# Must re-read settings after login
dis.fullscreen("Startup...")
settings.set_key()
settings.load()
settings.load(dis)
# implement "login countdown" feature
delay = settings.get('lgto', 0)
@ -672,7 +672,8 @@ async def start_login_sequence():
await block_until_login(rnd_keypad)
# implement idle timeout now that we are logged-in
loop.create_task(idle_logout())
from imptask import IMPT
IMPT.start_task('idle', idle_logout())
# Do green-light set immediately after firmware upgrade
if not pa.is_secondary:
@ -710,16 +711,14 @@ async def start_login_sequence():
# Allow USB protocol, now that we are auth'ed
from usb import enable_usb
enable_usb(loop, False)
goto_top_menu()
enable_usb()
def goto_top_menu():
# Start/restart menu system
from menu import MenuSystem
from flow import VirginSystem, NormalSystem, EmptyWallet, FactoryMenu
from main import pa, hsm_active
from glob import hsm_active
if hsm_active:
from hsm_ux import hsm_ux_obj
@ -874,8 +873,7 @@ def import_from_dice(*a):
async def import_xprv(*A):
# read an XPRV from a text file and use it.
import tcc, chains, ure
from main import pa
import ngu, chains, ure
from stash import SecretStash
from ubinascii import hexlify as b2a_hex
from backups import restore_from_dict
@ -903,7 +901,8 @@ async def import_xprv(*A):
node, chain, addr_fmt = None, None, None
# open file and do it
pat=ure.compile(r'.prv[A-Za-z0-9]+')
pat = ure.compile(r'.prv[A-Za-z0-9]+')
node = None
with CardSlot() as card:
with open(fn, 'rt') as fd:
for ln in fd.readlines():
@ -911,22 +910,13 @@ async def import_xprv(*A):
found = pat.search(ln)
if not found: continue
found = found.group(0)
for ch in chains.AllChains:
for kk in ch.slip132:
if found[0] == ch.slip132[kk].hint:
try:
node = tcc.bip32.deserialize(found,
ch.slip132[kk].pub, ch.slip132[kk].priv)
chain = ch
addr_fmt = kk
break
except ValueError:
pass
if node:
try:
node, chain, addr_fmt, is_priv = chains.slip32_deserialize(found)
break
except:
continue
if not node:
# unable
@ -939,9 +929,10 @@ the start of a line, and probably starts with "xprv".''', title="FAILED")
d = dict(chain=chain.ctype, raw_secret=b2a_hex(SecretStash.encode(xprv=node)))
node.blank()
# SHould capture the address format implied by SLIP32 version bytes
# Should capture the address format implied by SLIP32 version bytes
# (addr_fmt var here) but no means to store that in our settings, and we're
# not supposed to care anyway.
# TODO: would be nice for addr explorer tho
# restore as if it was a backup (code reuse)
await restore_from_dict(d)
@ -954,7 +945,6 @@ the seed value and the old seed would be lost.\n\n\
Visit the advanced menu and choose 'Destroy Seed'.'''
async def restore_everything(*A):
from main import pa
if not pa.is_secret_blank():
await ux_show_story(EMPTY_RESTORE_MSG)
@ -970,7 +960,6 @@ async def restore_everything(*A):
async def restore_everything_cleartext(*A):
# Asssume no password on backup file; devs and crazy people only
from main import pa
if not pa.is_secret_blank():
await ux_show_story(EMPTY_RESTORE_MSG)
@ -1011,9 +1000,9 @@ async def list_files(*A):
fn = await file_picker('Lists all files on MicroSD. Select one and SHA256(file contents) will be shown.', min_size=0)
if not fn: return
import tcc
from uhashlib import sha256
from utils import B2A
chk = tcc.sha256()
chk = sha256()
try:
with CardSlot() as card:
@ -1153,7 +1142,7 @@ async def check_firewall_read(*a):
async def bless_flash(*a):
# make green LED turn on
from main import pa, dis
from glob import dis
if pa.is_secondary:
await needs_primary()
@ -1252,7 +1241,7 @@ async def pin_changer(_1, _2, item):
# - there is a duress wallet for both main/sec pins, and you need to know main pin for that
# - what may look like just policy here, is in fact enforced by the bootrom code
#
from main import pa, dis
from glob import dis
from login import LoginUX
from pincodes import BootloaderError, EPIN_OLD_AUTH_FAIL
@ -1438,7 +1427,6 @@ We strongly recommend all PIN codes used be unique between each other.
async def show_version(*a):
# show firmware, bootload versions.
from main import settings
import callgate, version
from ubinascii import hexlify as b2a_hex
@ -1481,7 +1469,8 @@ async def ship_wo_bag(*a):
if not ok: return
import callgate
from main import dis, pa, is_devmode
from glob import dis
from version import is_devmode
failed = callgate.set_bag_number(b'NOT BAGGED') # 32 chars max

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# address_explorer.py
#
@ -12,6 +11,7 @@ from menu import MenuSystem, MenuItem, start_chooser
from public_constants import AFC_BECH32, AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
from multisig import MultisigWallet
from uasyncio import sleep_ms
from nvstore import settings
def truncate_address(addr):
# Truncates address to width of screen, replacing middle chars
@ -119,7 +119,7 @@ class AddressListMenu(MenuSystem):
async def render(self):
# Choose from a truncated list of index 0 common addresses, remember
# the last address the user selected and use it as the default
from main import settings, dis
from glob import dis
chain = chains.current_chain()
dis.fullscreen('Wait...')
@ -169,13 +169,11 @@ class AddressListMenu(MenuSystem):
await self.render()
async def pick_single(self, _1, menu_idx, item):
from main import settings
settings.put('axi', menu_idx) # update last clicked address
path, addr_fmt = item.arg
await self.show_n_addresses(path, addr_fmt, None)
async def pick_multisig(self, _1, menu_idx, item):
from main import settings
ms_wallet = item.arg
settings.put('axi', menu_idx) # update last clicked address
await self.show_n_addresses(None, None, ms_wallet)
@ -205,7 +203,7 @@ Press 3 if you really understand and accept these risks.
# Displays n addresses by replacing {idx} in path format.
# - also for other {account} numbers
# - or multisig case
from main import dis
from glob import dis
import version
def make_msg():
@ -274,7 +272,7 @@ Press 3 if you really understand and accept these risks.
if ch == '1':
# save addresses to MicroSD signal
await make_address_summary_file(path, addr_fmt, ms_wallet,
await make_address_summary_file(path, addr_fmt, ms_wallet, self.account_num,
count=(250 if n!=1 else 1))
# .. continue on same screen in case they want to write to multiple cards
continue
@ -301,7 +299,7 @@ Press 3 if you really understand and accept these risks.
msg, addrs = make_msg()
def generate_address_csv(path, addr_fmt, ms_wallet, n, start=0, account_num=0):
def generate_address_csv(path, addr_fmt, ms_wallet, account_num, n, start=0):
# Produce CSV file contents as a generator
if ms_wallet:
@ -333,9 +331,9 @@ def generate_address_csv(path, addr_fmt, ms_wallet, n, start=0, account_num=0):
stash.blank_object(node)
async def make_address_summary_file(path, addr_fmt, ms_wallet, count=250):
async def make_address_summary_file(path, addr_fmt, ms_wallet, account_num, count=250):
# write addresses into a text file on the MicroSD
from main import dis
from glob import dis
from files import CardSlot, CardMissingError
from actions import needs_microsd
@ -346,7 +344,7 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, count=250):
fname_pattern='addresses.csv'
# generator function
body = generate_address_csv(path, addr_fmt, ms_wallet, count)
body = generate_address_csv(path, addr_fmt, ms_wallet, account_num, count)
# pick filename and write
try:
@ -374,7 +372,6 @@ async def make_address_summary_file(path, addr_fmt, ms_wallet, count=250):
async def address_explore(*a):
# explore addresses based on derivation path chosen
# by proxy external index=0 address
from main import settings
while not settings.get('axskip', False):
ch = await ux_show_story('''\

View File

@ -1,10 +1,9 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# Operations that require user authorization, like our core features: signing messages
# and signing bitcoin transactions.
#
import stash, ure, tcc, ux, chains, sys, gc, uio, version
import stash, ure, ux, chains, sys, gc, uio, version, ngu
from public_constants import MAX_TXN_LEN, MSG_SIGNING_MAX_LENGTH, SUPPORTED_ADDR_FORMATS
from public_constants import AFC_SCRIPT, AF_CLASSIC, AFC_BECH32, AF_P2WPKH
from public_constants import STXN_FLAGS_MASK, STXN_FINALIZE, STXN_VISUALIZE, STXN_SIGNED
@ -93,7 +92,7 @@ class UserAuthorizedAction:
msg += '\n\n'
msg += problem_file_line(exc)
from main import hsm_active, dis
from glob import hsm_active, dis
# do nothing more for HSM case: msg will be available over USB
if hsm_active:
@ -134,7 +133,7 @@ RFC_SIGNATURE_TEMPLATE = '''\
def sign_message_digest(digest, subpath, prompt):
# do the signature itself!
from main import dis
from glob import dis
if prompt:
dis.fullscreen(prompt, percent=.25)
@ -143,11 +142,11 @@ def sign_message_digest(digest, subpath, prompt):
dis.progress_bar_show(.50)
node = sv.derive_path(subpath)
pk = node.private_key()
pk = node.privkey()
sv.register(pk)
dis.progress_bar_show(.75)
rv = tcc.secp256k1.sign(pk, digest)
rv = ngu.secp256k1.sign(pk, digest).to_bytes()
dis.progress_bar_show(1)
@ -160,7 +159,7 @@ class ApproveMessageSign(UserAuthorizedAction):
self.subpath = subpath
self.approved_cb = approved_cb
from main import dis
from glob import dis
dis.fullscreen('Wait...')
with stash.SensitiveValues() as sv:
@ -171,7 +170,7 @@ class ApproveMessageSign(UserAuthorizedAction):
async def interact(self):
# Prompt user w/ details and get approval
from main import dis, hsm_active
from glob import dis, hsm_active
if hsm_active:
ch = await hsm_active.approve_msg_sign(self.text, self.address, self.subpath)
@ -384,12 +383,12 @@ class ApproveTransaction(UserAuthorizedAction):
async def interact(self):
# Prompt user w/ details and get approval
from main import dis, hsm_active
from glob import dis, hsm_active
# step 1: parse PSBT from sflash into in-memory objects.
dis.fullscreen("Validating...")
try:
dis.fullscreen("Reading...")
with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len) as fd:
self.psbt = psbtObject.read_psbt(fd)
except BaseException as exc:
@ -401,6 +400,8 @@ class ApproveTransaction(UserAuthorizedAction):
return await self.failure(msg, exc)
dis.fullscreen("Validating...")
# Do some analysis/ validation
try:
await self.psbt.validate() # might do UX: accept multisig import
@ -502,7 +503,8 @@ class ApproveTransaction(UserAuthorizedAction):
# do the actual signing.
try:
gc.collect()
dis.fullscreen('Wait...')
gc.collect() # visible delay causes by this but also sign_it() below
self.psbt.sign_it()
except FraudulentChangeOutput as exc:
return await self.failure(exc.args[0], title='Change Fraud')
@ -561,7 +563,7 @@ class ApproveTransaction(UserAuthorizedAction):
if chk:
from ubinascii import b2a_base64
# append the signature
digest = tcc.sha256(chk.digest()).digest()
digest = ngu.hash.sha256s(chk.digest())
sig = sign_message_digest(digest, 'm', None)
fd.write(b2a_base64(sig).decode('ascii').strip())
fd.write('\n')
@ -682,7 +684,7 @@ def sign_transaction(psbt_len, flags=0x0, psbt_sha=None):
def sign_psbt_file(filename):
# sign a PSBT file found on a MicroSD card
from files import CardSlot, CardMissingError, securely_blank_file
from main import dis
from glob import dis
from sram2 import tmp_buf
from utils import HexStreamer, Base64Streamer, HexWriter, Base64Writer
@ -754,7 +756,7 @@ def sign_psbt_file(filename):
out_fn = None
txid = None
from main import settings
from nvstore import settings
import os
del_after = settings.get('del', 0)
@ -900,7 +902,7 @@ class NewPassphrase(UserAuthorizedAction):
async def interact(self):
# prompt them
from main import settings
from nvstore import settings
showit = False
while 1:
@ -960,7 +962,7 @@ class ShowAddressBase(UserAuthorizedAction):
def __init__(self, *args):
super().__init__()
from main import dis
from glob import dis
dis.fullscreen('Wait...')
# this must set self.address and do other slow setup
@ -968,7 +970,7 @@ class ShowAddressBase(UserAuthorizedAction):
async def interact(self):
# Just show the address... no real confirmation needed.
from main import hsm_active, dis
from glob import hsm_active, dis
if not hsm_active:
msg = self.get_msg()
@ -1076,7 +1078,7 @@ def start_show_address(addr_format, subpath):
# require a path to a key
subpath = cleanup_deriv_path(subpath)
from main import hsm_active
from glob import hsm_active
if hsm_active and not hsm_active.approve_address_share(subpath):
raise HSMDenied

View File

@ -1,16 +1,17 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# backups.py - Save and restore backup data.
#
import compat7z, stash, tcc, ckcc, chains, gc, sys
import compat7z, stash, ckcc, chains, gc, sys, bip39, uos, ngu
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from utils import imported, xfp2str
from ux import ux_show_story, ux_confirm
from ux import ux_show_story, ux_confirm, ux_dramatic_pause
import version, ujson
from uio import StringIO
import seed
from nvstore import settings
from pincodes import pa, AE_SECRET_LEN
# we make passwords with this number of words
num_pw_words = const(12)
@ -23,7 +24,6 @@ def render_backup_contents():
# key = value
# or #comments
# but value is JSON
from main import settings, pa
rv = StringIO()
@ -45,7 +45,7 @@ def render_backup_contents():
with stash.SensitiveValues(bypass_pw=True) as sv:
if sv.mode == 'words':
ADD('mnemonic', tcc.bip39.from_data(sv.raw))
ADD('mnemonic', bip39.b2a_words(sv.raw))
if sv.mode == 'master':
ADD('bip32_master_key', b2a_hex(sv.raw))
@ -95,8 +95,7 @@ def render_backup_contents():
async def restore_from_dict(vals):
# Restore from a dict of values. Already JSON decoded.
# Reboot on success, return string on failure
from main import pa, dis, settings
from pincodes import AE_SECRET_LEN
from glob import dis
#print("Restoring from: %r" % vals)
@ -176,7 +175,7 @@ async def restore_from_dict(vals):
await ux_show_story('Everything has been successfully restored. '
'We must now reboot to install the '
'updated settings and/or seed.', title='Success!')
'updated settings and seed.', title='Success!')
from machine import reset
reset()
@ -189,7 +188,7 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
b = bytearray(32)
while 1:
ckcc.rng_bytes(b)
words = tcc.bip39.from_data(b).split(' ')[0:num_pw_words]
words = bip39.b2a_words(b).split(' ')[0:num_pw_words]
ch = await seed.show_words(words,
prompt="Record this (%d word) backup file password:\n", escape='6')
@ -215,11 +214,11 @@ async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False):
ch = await seed.word_quiz(words, limited=(num_pw_words//3))
if ch == 'x': return
return await write_complete_backup(words, fname_pattern, write_sflash)
return await write_complete_backup(words, fname_pattern, write_sflash=write_sflash)
async def write_complete_backup(words, fname_pattern, write_sflash):
async def write_complete_backup(words, fname_pattern, write_sflash=False, allow_copies=True):
# Just do the writing
from main import dis, pa, settings
from glob import dis
from files import CardSlot, CardMissingError
# Show progress:
@ -236,7 +235,12 @@ async def write_complete_backup(words, fname_pattern, write_sflash):
zz = compat7z.Builder(password=pw, progress_fcn=dis.progress_bar_show)
zz.add_data(body)
hdr, footer = zz.save('ckcc-backup.txt')
# pick random filename, but ending in .txt
word = bip39.wordlist_en[ngu.random.uniform(2048)]
num = ngu.random.uniform(1000)
fname = '%s%d.txt' % (word, num)
hdr, footer = zz.save(fname)
filesize = len(body) + MAX_BACKUP_FILE_SIZE
@ -290,6 +294,9 @@ async def write_complete_backup(words, fname_pattern, write_sflash):
if ch == 'x': break
continue
if not allow_copies:
return
if copy == 0:
while 1:
msg = '''Backup file written:\n\n%s\n\n\
@ -330,7 +337,7 @@ async def verify_backup_file(fname_or_fd):
assert len(files) == 1
fname, fsize = files[0]
assert fname == 'ckcc-backup.txt'
assert fname.endswith('.txt')
assert 400 < fsize < MAX_BACKUP_FILE_SIZE, 'size'
except CardMissingError:
@ -363,11 +370,11 @@ async def restore_complete(fname_or_fd):
the_ux.push(m)
async def restore_complete_doit(fname_or_fd, words):
async def restore_complete_doit(fname_or_fd, words, file_cleanup=None):
# Open file, read it, maybe decrypt it; return string if any error
# - some errors will be shown, None return in that case
# - no return if successful (due to reboot)
from main import dis
from glob import dis
from files import CardSlot, CardMissingError
from actions import needs_microsd
@ -401,7 +408,7 @@ async def restore_complete_doit(fname_or_fd, words):
progress_fcn=dis.progress_bar_show)
# simple quick sanity checks
assert fname == 'ckcc-backup.txt'
assert fname.endswith('.txt') # was == 'ckcc-backup.txt'
assert contents[0:1] == b'#' and contents[-1:] == b'\n'
except Exception as e:
@ -412,6 +419,10 @@ async def restore_complete_doit(fname_or_fd, words):
'\n\nTried:\n\n' + password)
finally:
fd.close()
if file_cleanup:
file_cleanup(fname_or_fd)
except CardMissingError:
await needs_microsd()
return
@ -433,4 +444,135 @@ async def restore_complete_doit(fname_or_fd, words):
# this leads to reboot if it works, else errors shown, etc.
return await restore_from_dict(vals)
async def clone_start(*a):
# Begins cloning process, on target device.
from files import CardSlot, CardMissingError
ch = await ux_show_story('''Insert a MicroSD card and press OK to start. A small \
file with an ephemeral public key will be written.''')
if ch != 'y': return
# pick a random key pair, just for this cloning session
pair = ngu.secp256k1.keypair()
my_pubkey = pair.pubkey().to_bytes(False)
# write to SD Card, fixed filename for ease of use
try:
with CardSlot() as card:
fname, nice = card.pick_filename('ccbk-start.json', overwrite=True)
with open(fname, 'wb') as fd:
fd.write(ujson.dumps(dict(pubkey=b2a_hex(my_pubkey))))
except CardMissingError:
await needs_microsd()
return
except Exception as e:
await ux_show_story('Error: ' + str(e))
return
# Wait for incoming clone file, allow retries
ch = await ux_show_story('''Keep power on this Coldcard, and take MicroSD card \
to source Coldcard. Select Advanced > MicroSD > Clone Coldcard to write to card. Bring that card \
back and press OK to complete clone process.''')
while 1:
if ch != 'y':
# try to clean up, but card probably not there? No errors.
try:
with CardSlot() as card:
uos.remove(fname)
except:
pass
await ux_dramatic_pause('Aborted.', 2)
return
# Hopefully we have a suitable 7z file now. Pubkey in the filename
incoming = None
try:
with CardSlot() as card:
for path in card.get_paths():
for fn, ftype, *var in uos.ilistdir(path):
if fn.endswith('-ccbk.7z'):
incoming = path + '/' + fn
his_pubkey = a2b_hex(fn[0:66])
assert len(his_pubkey) == 33
assert 2 <= his_pubkey[0] <= 3
break
except CardMissingError:
await needs_microsd()
continue
except Exception as e:
pass
if incoming:
break
ch = await ux_show_story("Clone file not found. OK to try again, X to stop.")
# calculate point
session_key = pair.ecdh_multiply(his_pubkey)
# "password" is that hex value
words = [b2a_hex(session_key).decode()]
def delme(xfn):
# Callback to delete file after its read; could still fail but
# need to start over in that case anyway.
uos.remove(xfn)
uos.remove(fname) # ccbk-start.json
# this will reset in successful case, no return (but delme is called)
prob = await restore_complete_doit(incoming, words, file_cleanup=delme)
if prob:
await ux_show_story(prob, title='FAILED')
async def clone_write_data(*a):
# Write encrypted backup file, for cloning purposes, based on a public key
# found on the SD Card.
# - input file must already exist on inserted card
from files import CardSlot, CardMissingError
try:
with CardSlot() as card:
path = card.get_sd_root()
with open(path + '/ccbk-start.json', 'rb') as fd:
d = ujson.load(fd)
his_pubkey = a2b_hex(d.get('pubkey'))
# expect compress pubkey
assert len(his_pubkey) == 33
assert 2 <= his_pubkey[0] <= 3
# remove any other clone-files on this card, so no confusion
# on receiving end; unlikely they can work anyway since new key each time
for path in card.get_paths():
for fn, ftype, *var in uos.ilistdir(path):
if fn.endswith('-ccbk.7z'):
try:
uos.remove(path + '/' + fn)
except:
pass
except (CardMissingError, OSError) as exc:
# Standard msg shown if no SD card detected when we need one.
await ux_show_story("Start this process on the other Coldcard, which will write a file onto MicroSD card as the first step.\n\nInsert that card and try again here.")
return
# pick our own temp keys for this encryption
pair = ngu.secp256k1.keypair()
my_pubkey = pair.pubkey().to_bytes(False)
session_key = pair.ecdh_multiply(his_pubkey)
words = [b2a_hex(session_key).decode()]
fname = b2a_hex(my_pubkey).decode() + '-ccbk.7z'
await write_complete_backup(words, fname, allow_copies=False)
await ux_show_story("Done.\n\nTake this MicroSD card back to other Coldcard and continue from there.")
# EOF

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# callgate.py - thin wrapper around modckcc and the bootloader and its services.
#

View File

@ -1,9 +1,9 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# chains.py - Magic values for the coins and altcoins we support
#
import tcc
import ngu
from uhashlib import sha256
from public_constants import AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH
from public_constants import AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT, AFC_WRAPPED
from serializations import hash160, ser_compact_size
@ -43,19 +43,23 @@ class ChainsBase:
@classmethod
def serialize_private(cls, node, addr_fmt=AF_CLASSIC):
# output a xprv
return node.serialize_private(cls.slip132[addr_fmt].priv)
return node.serialize(cls.slip132[addr_fmt].priv, True)
@classmethod
def serialize_public(cls, node, addr_fmt=AF_CLASSIC):
# output a xpub
addr_fmt = AF_CLASSIC if addr_fmt == AF_P2SH else addr_fmt
return node.serialize_public(cls.slip132[addr_fmt].pub)
return node.serialize(cls.slip132[addr_fmt].pub, False)
@classmethod
def deserialize_node(cls, text, addr_fmt):
# xpub/xprv to object
addr_fmt = AF_CLASSIC if addr_fmt == AF_P2SH else addr_fmt
return tcc.bip32.deserialize(text, cls.slip132[addr_fmt].pub, cls.slip132[addr_fmt].priv)
node = ngu.hdnode.HDNode()
version = node.deserialize(text)
assert (version == cls.slip132[addr_fmt].pub) \
or (version == cls.slip132[addr_fmt].priv)
return node
@classmethod
def p2sh_address(cls, addr_fmt, witdeem_script):
@ -71,19 +75,19 @@ class ChainsBase:
assert witdeem_script, "need witness/redeem script"
if addr_fmt & AFC_SEGWIT:
digest = tcc.sha256(witdeem_script).digest()
digest = ngu.hash.sha256s(witdeem_script)
else:
digest = hash160(witdeem_script)
if addr_fmt & AFC_BECH32:
# bech32 encoded segwit p2sh
addr = tcc.codecs.bech32_encode(cls.bech32_hrp, 0, digest)
addr = ngu.codecs.segwit_encode(cls.bech32_hrp, 0, digest)
elif addr_fmt == AF_P2WSH_P2SH:
# segwit p2wsh encoded as classic P2SH
addr = tcc.codecs.b58_encode(cls.b58_script + hash160(b'\x00\x20' + digest))
addr = ngu.codecs.b58_encode(cls.b58_script + hash160(b'\x00\x20' + digest))
else:
# P2SH classic
addr = tcc.codecs.b58_encode(cls.b58_script + digest)
addr = ngu.codecs.b58_encode(cls.b58_script + digest)
return addr
@ -94,7 +98,7 @@ class ChainsBase:
if addr_fmt == AF_CLASSIC:
# olde fashioned P2PKH
assert len(cls.b58_addr) == 1
return node.address(cls.b58_addr[0])
return node.addr_help(cls.b58_addr[0])
if addr_fmt & AFC_SCRIPT:
# use p2sh_address() instead.
@ -102,30 +106,30 @@ class ChainsBase:
# so must be P2PKH, fetch it.
assert addr_fmt & AFC_PUBKEY
raw = node.address_raw()
raw = node.addr_help()
assert len(raw) == 20
if addr_fmt & AFC_BECH32:
# bech32 encoded segwit p2pkh
return tcc.codecs.bech32_encode(cls.bech32_hrp, 0, raw)
return ngu.codecs.segwit_encode(cls.bech32_hrp, 0, raw)
# see bip-141, "P2WPKH nested in BIP16 P2SH" section
# see BIP-141, "P2WPKH nested in BIP16 P2SH" section
assert addr_fmt == AF_P2WPKH_P2SH
assert len(cls.b58_script) == 1
digest = hash160(b'\x00\x14' + raw)
return tcc.codecs.b58_encode(cls.b58_script + digest)
return ngu.codecs.b58_encode(cls.b58_script + digest)
@classmethod
def privkey(cls, node):
# serialize a private key (generally shouldn't be!)
return node.serialize_private(cls.b58_privkey)
return node.serialize(cls.b58_privkey, True)
@classmethod
def hash_message(cls, msg=None, msg_len=0):
# Perform sha256 for message-signing purposes (only)
# - or get setup for that, if msg == None
s = tcc.sha256()
s = sha256()
s.update(cls.msg_signing_prefix())
@ -138,7 +142,7 @@ class ChainsBase:
s.update(msg)
return tcc.sha256(s.digest()).digest()
return ngu.hash.sha256s(s.digest())
@classmethod
@ -169,19 +173,19 @@ class ChainsBase:
# P2PKH
if ll == 25 and script[0:3] == b'\x76\xA9\x14' and script[23:26] == b'\x88\xAC':
return tcc.codecs.b58_encode(cls.b58_addr + script[3:3+20])
return ngu.codecs.b58_encode(cls.b58_addr + script[3:3+20])
# P2SH
if ll == 23 and script[0:2] == b'\xA9\x14' and script[22] == 0x87:
return tcc.codecs.b58_encode(cls.b58_script + script[2:2+20])
return ngu.codecs.b58_encode(cls.b58_script + script[2:2+20])
# P2WPKH
if ll == 22 and script[0:2] == b'\x00\x14':
return tcc.codecs.bech32_encode(cls.bech32_hrp, 0, script[2:])
return ngu.codecs.segwit_encode(cls.bech32_hrp, 0, script[2:])
# P2WSH
if ll == 34 and script[0:2] == b'\x00\x20':
return tcc.codecs.bech32_encode(cls.bech32_hrp, 0, script[2:])
return ngu.codecs.segwit_encode(cls.bech32_hrp, 0, script[2:])
raise ValueError('Unknown payment script', repr(script))
@ -251,12 +255,27 @@ def get_chain(short_name, btc_default=False):
def current_chain():
# return chain matching current setting
from main import settings
from nvstore import settings
chain = settings.get('chain', 'BTC')
return get_chain(chain)
def slip32_deserialize(xp):
# .. and classify chain and addr-type, as implied by prefix
node = ngu.hdnode.HDNode()
version = node.deserialize(xp)
for ch in AllChains:
for kk in ch.slip132:
if ch.slip132[kk].pub == version:
return node, ch, kk, False
if ch.slip132[kk].priv == version:
return node, ch, kk, True
raise ValueError(hex(version))
# Some common/useful derivation paths and where they may be used.
# see bip49 for meaning of the meta vars
CommonDerivations = [

View File

@ -1,9 +1,8 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# choosers.py - various interactive menus for setting config values.
#
from main import settings
from nvstore import settings
def max_fee_chooser():
from psbt import DEFAULT_MAX_FEE_PERCENTAGE

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# compat7z.py
#
@ -7,13 +6,13 @@
# always does AES-256. Not really expecting to be able to read any 7z file, except
# those we created ourselves.
#
import os, sys, tcc, ckcc
import os, sys, ckcc, ngu
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from ubinascii import crc32
from ustruct import unpack, pack, calcsize
from ucollections import namedtuple
from tcc import sha256 # uhashlib also works
from uhashlib import sha256
from uio import BytesIO
def masked_crc(bits):
@ -263,11 +262,13 @@ class Builder(object):
# figure out key to be used
key = self.calculate_key(password, progress_fcn)
out = b''
aes = tcc.AES(tcc.AES.CBC | tcc.AES.Decrypt, key, self.iv)
aes = ngu.aes.CBC(False, key, self.iv)
out = b''
for blk in range(0, len(body), 16):
out += aes.update(body[blk:blk+16])
out += aes.cipher(body[blk:blk+16])
aes.blank()
# trim padding, check CRC
out = out[0:unpacked_size]
@ -290,9 +291,9 @@ class Builder(object):
fname, body_size, unpacked_size, expect_crc = self.parse_section_hdr(meta)
assert len(body) == body_size
assert unpacked_size <= max_size, 'too big'
assert len(body) <= unpacked_size+16, 'too big, encoded'
assert len(body) % 16 == 0, 'not blocked'
assert unpacked_size <= max_size # 'too big'
assert len(body) <= unpacked_size+16 # 'too big, encoded'
assert len(body) % 16 == 0 # 'not blocked'
#print("Section ok: '%s' of %d bytes => %r" % (fname, unpacked_size, shdr))
@ -306,8 +307,7 @@ class Builder(object):
def add_data(self, raw):
if not self.aes:
# do this late, so easier to test w/ known values.
#self.aes = AES.AESCipher(self.key, mode=AES.MODE_CBC, IV=self.iv)
self.aes = tcc.AES(tcc.AES.CBC | tcc.AES.Encrypt, self.key, self.iv)
self.aes = ngu.aes.CBC(True, self.key, self.iv)
here = len(raw)
self.pt_crc = crc32(raw, self.pt_crc)
@ -315,13 +315,13 @@ class Builder(object):
padded_len = (here + 15) & ~15
if padded_len != here:
if self.padding != None:
raise ValueError("can't do less than a block except at end")
raise ValueError() # "can't do less than a block except at end"
self.padding = (padded_len - here)
raw += '\x00' * self.padding
raw += bytes(self.padding)
self.unpacked_size += here
assert len(raw) % 16 == 0, b2a_hex(raw)
self.body += self.aes.update(raw)
assert len(raw) % 16 == 0
self.body += self.aes.cipher(raw)
def calculate_key(self, password, progress_fcn=None):

View File

@ -1,5 +1,4 @@
# (c) Copyright 2019 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# descriptor.py - Bitcoin Core's descriptors and their specialized checksums.
#

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# dev_helper.py - Debug and similar code.
#
@ -35,9 +34,12 @@ async def usb_keypad_emu():
# - code is **not** used in real product, but left here for devs to use
# - this code isn't even called; unless you add code to do so, see ../stm32/my_lib_boot2.py
#
await sleep_ms(1000) # avoid slowing the startup
from ux import the_ux
from menu import MenuSystem
from seed import WordNestMenu
import gc
u = pyb.USB_VCP()
@ -52,7 +54,7 @@ async def usb_keypad_emu():
await sleep_ms(100)
while u.isconnected() and u.any():
from main import numpad
from glob import numpad
k = u.read(3).decode()
@ -66,6 +68,10 @@ async def usb_keypad_emu():
print("Repl")
continue
if k == 'm':
print("free = %d" % gc.mem_free())
continue
if k in remap:
k = remap[k]

View File

@ -1,11 +1,11 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# display.py - OLED rendering
#
import machine, ssd1306, uzlib, ckcc
from ssd1306 import SSD1306_SPI
import framebuf
import uasyncio
from uasyncio import sleep_ms
from graphics import Graphics
from sram2 import display2_buf
@ -14,8 +14,6 @@ from sram2 import display2_buf
from zevvpeep import FontSmall, FontLarge, FontTiny
FontFixed = object() # ugly 8x8 PET font
#ARROWS = [chr(i) for i in range(8592, 8592+4)]
class Display:
WIDTH = 128
@ -125,17 +123,6 @@ class Display:
pos = min(int(mm*fraction), mm)
self.dis.fill_rect(128-2, pos, 1, 8, 1)
async def animate_splash(self, loop, done_fcn):
ns = 32
for i in range(ns):
self.progress_bar(i / (ns-1))
self.show()
await sleep_ms(3)
if done_fcn:
# create a new task, so this stack can be freed
loop.create_task(done_fcn())
def fullscreen(self, msg, percent=None, line2=None):
# show a simple message "fullscreen".
self.clear()
@ -178,10 +165,6 @@ class Display:
self.progress_bar(percent)
self.show()
def splash_animate(self, loop, done_fcn):
# do a bootup delay and some work along the way
loop.create_task(self.animate_splash(loop, done_fcn))
def mark_sensitive(self, from_y, to_y):
wx = self.WIDTH-4 # avoid scroll bar
for y in range(from_y, to_y):
@ -224,7 +207,7 @@ class Display:
self.show()
else:
# a pattern that repeats nices mod 128
# a pattern that repeats nicely mod 128
# - each byte here is a vertical column, 8 pixels tall, MSB at bottom
data = bytes(0x80 if (x%4)<2 else 0x0 for x in range(128))

View File

@ -1,5 +1,4 @@
# (c) Copyright 2020 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# BIP-85: Deterministic Entropy From BIP32 Keychains, by
# Ethan Kosakovsky <ethankosakovsky@protonmail.com>
@ -7,7 +6,7 @@
# Using the system's BIP32 master key, safely derive seeds phrases/entropy for other
# wallet systems, which may expect seed phrases, XPRV, or other entropy.
#
import stash, tcc, hmac, chains
import stash, ngu, chains, bip39
from ux import ux_show_story, ux_enter_number, the_ux, ux_confirm
from menu import MenuItem, MenuSystem
from ubinascii import hexlify as b2a_hex
@ -43,7 +42,7 @@ still backed-up.''')
the_ux.push(m)
def drv_entro_step2(_1, picked, _2):
from main import dis
from glob import dis
from files import CardSlot, CardMissingError
the_ux.pop()
@ -78,7 +77,8 @@ def drv_entro_step2(_1, picked, _2):
with stash.SensitiveValues() as sv:
node = sv.derive_path(path)
entropy = hmac.HMAC(b'bip-entropy-from-k', node.private_key(), tcc.sha512).digest()
entropy = ngu.hmac.hmac_sha512(b'bip-entropy-from-k', node.privkey())
sv.register(entropy)
# truncate for this application
@ -93,7 +93,7 @@ def drv_entro_step2(_1, picked, _2):
if s_mode == 'words':
# BIP39 seed phrase, various lengths
words = tcc.bip39.from_data(new_secret).split(' ')
words = bip39.b2a_words(new_secret).split(' ')
msg = 'Seed words (%d):\n' % len(words)
msg += '\n'.join('%2d: %s' % (i+1, w) for i,w in enumerate(words))
@ -109,13 +109,12 @@ def drv_entro_step2(_1, picked, _2):
# append 0x01 to indicate it's a compressed private key
pk = new_secret + b'\x01'
msg = 'WIF (privkey):\n' + tcc.codecs.b58_encode(chain.b58_privkey + pk)
msg = 'WIF (privkey):\n' + ngu.codecs.b58_encode(chain.b58_privkey + pk)
elif s_mode == 'xprv':
# Raw XPRV value.
ch, pk = new_secret[0:32], new_secret[32:64]
master_node = tcc.bip32.HDNode(chain_code=ch, private_key=pk,
child_num=0, depth=0, fingerprint=0)
master_node = ngu.hdnode.HDNode().from_chaincode_privkey(ch, pk)
encoded = stash.SecretStash.encode(xprv=master_node)
@ -135,7 +134,7 @@ def drv_entro_step2(_1, picked, _2):
if new_secret:
msg += '\n\nRaw Entropy:\n' + str(b2a_hex(new_secret), 'ascii')
print(msg) # XXX debug
#print(msg) # XXX debug
prompt = '\n\nPress 1 to save to MicroSD card'
if encoded is not None:
@ -169,8 +168,8 @@ def drv_entro_step2(_1, picked, _2):
stash.blank_object(msg)
if ch == '2' and (encoded is not None):
from main import pa, settings, dis
from pincodes import AE_SECRET_LEN
from glob import dis
from pincodes import pa, AE_SECRET_LEN
# switch over to new secret!
dis.fullscreen("Applying...")

View File

@ -1,5 +1,4 @@
# (c) Copyright 2020 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# exceptions.py - Exceptions defined by us.
#

View File

@ -1,15 +1,15 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# export.py - Export and share various semi-public data
#
import stash, tcc, chains, sys
import stash, chains, sys
#from ubinascii import hexlify as b2a_hex
#from ubinascii import unhexlify as a2b_hex
from utils import xfp2str
from utils import xfp2str, swab32
from ux import ux_show_story
import version, ujson
from uio import StringIO
from nvstore import settings
def generate_public_contents():
# Generate public details about wallet.
@ -18,7 +18,6 @@ def generate_public_contents():
# key = value
# or #comments
# but value is JSON
from main import settings
from public_constants import AF_CLASSIC
num_rx = 5
@ -27,6 +26,8 @@ def generate_public_contents():
with stash.SensitiveValues() as sv:
xfp = xfp2str(swab32(sv.node.my_fp()))
yield ('''\
# Coldcard Wallet Summary File
## For wallet with master key fingerprint: {xfp}
@ -50,7 +51,7 @@ be needed for different systems.
'''.format(nb=chain.name, xpub=chain.serialize_public(sv.node),
sym=chain.ctype, ct=chain.b44_cointype, xfp=xfp2str(sv.node.my_fingerprint())))
sym=chain.ctype, ct=chain.b44_cointype, xfp=xfp))
for name, path, addr_fmt in chains.CommonDerivations:
@ -116,7 +117,7 @@ be needed for different systems.
async def write_text_file(fname_pattern, body, title, total_parts=72):
# - total_parts does need not be precise
from main import dis, pa, settings
from glob import dis
from files import CardSlot, CardMissingError
from actions import needs_microsd
@ -142,7 +143,7 @@ async def write_text_file(fname_pattern, body, title, total_parts=72):
await ux_show_story(msg)
async def make_summary_file(fname_pattern='public.txt'):
from main import dis
from glob import dis
# record **public** values and helpful data into a text file
dis.fullscreen('Generating...')
@ -153,7 +154,7 @@ async def make_summary_file(fname_pattern='public.txt'):
await write_text_file(fname_pattern, body, 'Summary')
async def make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.txt'):
from main import dis, settings
from glob import dis
import ustruct
xfp = xfp2str(settings.get('xfp'))
@ -193,7 +194,6 @@ def generate_bitcoin_core_wallet(example_addrs, account_num):
# Generate the data for an RPC command to import keys into Bitcoin Core
# - yields dicts for json purposes
from descriptor import append_checksum
from main import settings
import ustruct
from public_constants import AF_P2WPKH
@ -239,7 +239,6 @@ def generate_bitcoin_core_wallet(example_addrs, account_num):
def generate_wasabi_wallet():
# Generate the data for a JSON file which Wasabi can open directly as a new wallet.
from main import settings
import ustruct, version
# bitcoin (xpub) is used, even for testnet case (ie. no tpub)
@ -264,7 +263,6 @@ def generate_wasabi_wallet():
def generate_generic_export(account_num=0):
# Generate data that other programers will use to import Coldcard (single-signer)
from main import settings
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
chain = chains.current_chain()
@ -284,13 +282,12 @@ def generate_generic_export(account_num=0):
]:
dd = deriv.format(ct=chain.b44_cointype, acc=account_num)
node = sv.derive_path(dd)
xfp = xfp2str(node.my_fingerprint())
xfp = xfp2str(swab32(node.my_fp()))
xp = chain.serialize_public(node, AF_CLASSIC)
zp = chain.serialize_public(node, fmt) if fmt != AF_CLASSIC else None
# bonus/check: first non-change address: 0/0
node.derive(0)
node.derive(0)
node.derive(0, False).derive(0, False)
rv[name] = dict(deriv=dd, xpub=xp, xfp=xfp, first=chain.address(node, fmt), name=atype)
if zp:
@ -303,7 +300,6 @@ def generate_electrum_wallet(addr_type, account_num=0):
#
# Much reverse enginerring of Electrum here. It's a complex
# legacy file format.
from main import settings
from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH
chain = chains.current_chain()
@ -346,7 +342,7 @@ def generate_electrum_wallet(addr_type, account_num=0):
async def make_json_wallet(label, generator, fname_pattern='new-wallet.json'):
# Record **public** values and helpful data into a JSON file
from main import dis, pa, settings
from glob import dis
from files import CardSlot, CardMissingError
from actions import needs_microsd

View File

@ -1,12 +1,10 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# files.py - MicroSD and related functions.
#
import pyb, ckcc, os, sys, utime
from uerrno import ENOENT
def _try_microsd(bad_fs_ok=False):
# Power up, mount the SD card, return False if we can't for some reason.
#
@ -44,7 +42,7 @@ def _try_microsd(bad_fs_ok=False):
def wipe_flash_filesystem():
# erase and re-format the flash filesystem (/flash/)
import ckcc, pyb
from main import dis, settings
from glob import dis
dis.fullscreen('Erasing...')
os.umount('/flash')
@ -74,13 +72,17 @@ def wipe_flash_filesystem():
dis.fullscreen('Rebuilding...')
ckcc.wipe_fs()
# re-store settings
# re-store current settings
from nvstore import settings
settings.save()
# remount it
os.mount(fl, '/flash')
def wipe_microsd_card():
# Erase and re-format SD card. Not secure erase, because that is too slow.
import ckcc, pyb
from main import dis
from glob import dis
try:
os.umount('/sd')
@ -105,7 +107,7 @@ def wipe_microsd_card():
sd.writeblocks(bnum, blk)
dis.progress_bar_show(bnum/cutoff)
dis.fullscreen('Formating...')
dis.fullscreen('Formatting...')
# remount, with newfs option
os.mount(sd, '/sd', readonly=0, mkfs=1)
@ -250,10 +252,18 @@ class CardSlot:
def get_id_hash(self):
# hash over card config and serial # details
import tcc
# - stupidly it's over the repr of a functions' result
import ngu
info = pyb.SDCard().info()
assert info and len(info) >= 5 # need micropython changes
return tcc.sha256(repr(info)).digest()
assert info
if len(info) == 3:
# expected in v4
csd_cid = pyb.SDCard().ident()
info = tuple(list(info) + list(csd_cid))
return ngu.hash.sha256s(repr(info))
def pick_filename(self, pattern, path=None, overwrite=False):
# given foo.txt, return a full path to filesystem, AND

View File

@ -1,11 +1,10 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# flow.py - Menu structure
#
from menu import MenuItem
import version
from main import settings
from nvstore import settings
from actions import *
from choosers import *
@ -13,6 +12,7 @@ from multisig import make_multisig_menu
from address_explorer import address_explore
from users import make_users_menu
from drv_entro import drv_entro_start
from backups import clone_start, clone_write_data
# Optional feature: HSM
if version.has_fatram:
@ -52,11 +52,11 @@ if not version.has_608:
async def which_pin_menu(_1,_2, item):
if version.has_608: return PinChangesMenu
from main import pa
from pincodes import pa
return PinChangesMenu if not pa.is_secondary else SecondaryPinChangesMenu
def has_secrets():
from main import pa
from pincodes import pa
return not pa.is_secret_blank()
SettingsMenu = [
@ -87,6 +87,7 @@ SDCardMenu = [
MenuItem('Export Wallet', menu=WalletExportMenu),
MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd),
MenuItem('Upgrade From SD', f=microsd_upgrade),
MenuItem('Clone Coldcard', predicate=has_secrets, f=clone_write_data),
MenuItem('List Files', f=list_files),
MenuItem('Format Card', f=wipe_sd_card),
]
@ -157,6 +158,7 @@ BackupStuffMenu = [
MenuItem("Backup System", f=backup_everything),
MenuItem("Verify Backup", f=verify_backup),
MenuItem("Restore Backup", f=restore_everything), # just a redirect really
MenuItem('Clone Coldcard', predicate=has_secrets, f=clone_write_data),
MenuItem("Dump Summary", f=dump_summary),
]
@ -182,10 +184,12 @@ VirginSystem = [
]
ImportWallet = [
# xxxxxxxxxxxxxxxx
MenuItem("24 Words", menu=start_seed_import, arg=24),
MenuItem("18 Words", menu=start_seed_import, arg=18),
MenuItem("12 Words", menu=start_seed_import, arg=12),
MenuItem("Restore Backup", f=restore_everything),
MenuItem("Clone Coldcard", menu=clone_start),
MenuItem("Import XPRV", f=import_xprv),
MenuItem("Dice Rolls", f=import_from_dice),
]

18
shared/glob.py Normal file
View File

@ -0,0 +1,18 @@
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# glob.py
#
# - simple module w/ handle for larger objects/singletons
# - used to be "from main import dis" and so on
# the display
dis = None
# the numpad
numpad = None
# global ptr to HSM policy, if any (supported on Mk3+ only)
hsm_active = None
# EOF

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# use:
# from h import *

View File

@ -1,15 +1,15 @@
# (c) Copyright 2020 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# history.py - store some history about past transactions and/or outputs they involved
#
import tcc, gc, chains
import gc, chains
from utils import B2A
from uhashlib import sha256
from ustruct import pack, unpack
from exceptions import IncorrectUTXOAmount
from ubinascii import b2a_base64, a2b_base64
from serializations import COutPoint, uint256_from_str
from main import settings
from nvstore import settings
# Very limited space in serial flash, so we compress as much as possible:
# - would be bad for privacy to store these **UTXO amounts** in plaintext
@ -60,7 +60,7 @@ class OutptValueCache:
# hash up the txid and output number, truncate, and encode as base64
# - truncating at (mod3) bytes so no padding on b64 output
# - expects a COutPoint
md = tcc.sha256('OutptValueCache')
md = sha256('OutptValueCache')
md.update(prevout.serialize())
return b2a_base64(md.digest()[:15])[:-1].decode()

View File

@ -1,97 +0,0 @@
# HMAC (Keyed-Hashing for Message Authentication) Python module.
#
# Implements the HMAC algorithm as described by RFC 2104.
#
# from: https://github.com/micropython/micropython-lib/blob/master/hmac/hmac.py @ 96c981b
# license: https://github.com/micropython/micropython-lib/blob/master/LICENSE
class HMAC:
"""RFC 2104 HMAC class. Also complies with RFC 4231.
This supports the API for Cryptographic Hash Functions (PEP 247).
"""
blocksize = 64 # 512-bit HMAC; can be changed in subclasses.
def __init__(self, key, msg = None, digestmod = None):
"""Create a new HMAC object.
key: key for the keyed hash object.
msg: Initial input for the hash, if provided.
digestmod: A module supporting PEP 247. *OR*
A hashlib constructor returning a new hash object. *OR*
Note: key and msg must be a bytes or bytearray objects.
"""
if not isinstance(key, (bytes, bytearray)):
raise TypeError()
self.outer = digestmod()
self.inner = digestmod()
self.digest_size = self.inner.digest_size
blocksize = self.inner.block_size
assert blocksize >= 16
# self.blocksize is the default blocksize. self.block_size is
# effective block size as well as the public API attribute.
self.block_size = blocksize
if len(key) > blocksize:
key = digestmod(key).digest()
def translate(d, t):
return bytes(t[x] for x in d)
trans_5C = bytes((x ^ 0x5C) for x in range(256))
trans_36 = bytes((x ^ 0x36) for x in range(256))
key = key + bytes(blocksize - len(key))
self.outer.update(translate(key, trans_5C))
self.inner.update(translate(key, trans_36))
if msg is not None:
self.update(msg)
def update(self, msg):
"""Update this hashing object with the string msg.
"""
self.inner.update(msg)
def _current(self):
"""Return a hash object for the current state.
To be used only internally with digest() and hexdigest().
"""
h = self.outer
h.update(self.inner.digest())
del self.outer
del self.inner
return h
def digest(self):
"""Return the hash value of this hashing object.
This returns a string containing 8-bit data.
Single use only!
"""
return self._current().digest()
def new(key, msg = None, digestmod = None):
"""Create a new hashing object and return it.
key: The starting key for the hash.
msg: if available, will immediately be hashed into the object's starting
state.
You can now feed arbitrary strings into the object using its update()
method, and can ask for the hash value at any time by calling its digest()
method.
"""
return HMAC(key, msg, digestmod)
# Useful ones for this project
#import tcc
#hmac_sha256 = lambda key, msg=None: HMAC(key, msg, tcc.sha256)
#hmac_sha1 = lambda key, msg=None: HMAC(key, msg, tcc.sha1)
#hmac_sha512 = lambda key, msg=None: HMAC(key, msg, tcc.sha512)

View File

@ -1,11 +1,10 @@
# (c) Copyright 2020 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# hsm.py
#
# Unattended signing of transactions and messages, subject to a set of rules.
#
import stash, ustruct, tcc, chains, sys, gc, uio, ujson, uos, utime, ckcc
import stash, ustruct, chains, sys, gc, uio, ujson, uos, utime, ckcc, ngu
from sffile import SFFile
from utils import problem_file_line, cleanup_deriv_path, match_deriv_path
from pincodes import AE_LONG_SECRET_LEN
@ -30,7 +29,7 @@ ABSOLUTE_MAX_REFUSALS = const(100)
# you have this many seconds after boot to escape HSM
# mode, if you enable the boot_to_hsm feature
BOOT_LOCKOUT_TIME = const(30)
BOOT_LOCKOUT_TIME = const(60)
def hsm_policy_available():
# Is there an HSM policy ready to go? Offer the menu item then.
@ -123,12 +122,12 @@ def cleanup_whitelist_value(s):
# - later matching is string-based, so just doing basic syntax check here
# - must be checksumed-base58 or bech32
try:
tcc.codecs.b58_decode(s)
ngu.codecs.b58_decode(s)
return s
except: pass
try:
tcc.codecs.bech32_decode(s)
ngu.codecs.segwit_decode(s)
return s
except: pass
@ -500,11 +499,12 @@ class HSMPolicy:
def activate(self, new_file):
# user approved the HSM activation, so apply it.
from main import pa, dis
from glob import dis
from pincodes import pa
import main
assert not main.hsm_active
main.hsm_active = self
import glob
assert not glob.hsm_active
glob.hsm_active = self
self.start_time = utime.ticks_ms()
@ -571,7 +571,7 @@ class HSMPolicy:
# save the "long secret" ... probably only happens first time HSM policy
# is activated, because we don't store that original value except here
# and in SE.
from main import pa
from pincodes import pa
# add length half-word to start, and pad to max size
tmp = bytearray(AE_LONG_SECRET_LEN)
@ -597,7 +597,7 @@ class HSMPolicy:
assert self.sl_reads < self.allow_sl, 'consumed'
self.sl_reads += 1
from main import pa
from pincodes import pa
raw = pa.ls_fetch()
ll, = ustruct.unpack_from('H', raw)
assert 0 <= ll <= AE_LONG_SECRET_LEN-2
@ -617,7 +617,7 @@ class HSMPolicy:
async def approve_msg_sign(self, msg_text, address, subpath):
# Maybe approve indicated message to be signed.
# return 'y' or 'x'
sha = tcc.sha256(msg_text).digest()
sha = ngu.hash.sha256s(msg_text)
with AuditLogger('messages', sha, self.never_log) as log:
if self.must_log and log.is_unsaved:
@ -681,7 +681,6 @@ class HSMPolicy:
if code == self.boot_to_hsm:
# let them out of jail
from hsm_ux import hsm_ux_obj
hsm_delete_policy()
hsm_ux_obj.test_restart = True
def consume_local_code(self, psbt_sha):
@ -701,7 +700,7 @@ class HSMPolicy:
# provide a random key to be used as HMAC key to generate the local code
# - want to keep this relatively short, and free of padding chars
from ubinascii import b2a_base64
self.next_local_code = b2a_base64(tcc.random.bytes(15)).strip().decode('ascii')
self.next_local_code = b2a_base64(ngu.random.bytes(15)).strip().decode('ascii')
async def approve_transaction(self, psbt, psbt_sha, story):
# Approve or don't a transaction. Catch assertions and other
@ -807,9 +806,8 @@ class HSMPolicy:
# Crash if too many refusals happen.
if self.refusals >= ABSOLUTE_MAX_REFUSALS:
from utils import clean_shutdown
from main import loop
loop.call_later_ms(250, clean_shutdown)
from utils import clean_shutdown, call_later_ms
call_later_ms(250, clean_shutdown)
def approve(self, log, msg):
# when things fail
@ -822,7 +820,8 @@ def hsm_status_report():
# Return a JSON-able object. Documented and external programs
# rely on this output... and yet, don't overshare either.
from auth import UserAuthorizedAction
from main import hsm_active, settings
from glob import hsm_active
from nvstore import settings
from hsm_ux import ApproveHSMPolicy
rv = dict()

View File

@ -1,18 +1,17 @@
# (c) Copyright 2020 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# hsm_ux.py
#
# User experience related to the HSM. Ironic because there isn't a user present.
#
import ustruct, tcc, ux, chains, sys, gc, uio, ujson, uos, utime
import ustruct, ux, chains, sys, gc, uio, ujson, uos, utime, ngu
from ckcc import is_simulator
from sffile import SFFile
from ux import ux_aborted, ux_show_story, abort_and_goto, ux_dramatic_pause, ux_clear_keys, the_ux
from ux import AbortInteraction
from utils import problem_file_line, cleanup_deriv_path
from auth import UserAuthorizedAction
from uasyncio.queues import QueueEmpty
from queues import QueueEmpty
from ubinascii import a2b_base64
from users import Users, MAX_NUMBER_USERS
from public_constants import MAX_USERNAME_LEN
@ -60,7 +59,7 @@ class ApproveHSMPolicy(UserAuthorizedAction):
self.refused = (ch != 'y')
if not self.refused and self.new_file:
confirm_char = '12346'[tcc.random.uniform(5)]
confirm_char = '12346'[ngu.random.uniform(5)]
msg = '''Last chance. You are defining a new policy which \
allows the Coldcard to sign specific transactions without any further user approval.\n\n\
Press %s to save policy and enable HSM mode.''' % confirm_char
@ -177,7 +176,7 @@ class hsmUxInteraction:
def draw_background(self):
# Render and capture static parts of screen one-time.
from main import dis
from glob import dis
from display import FontTiny
dis.clear()
@ -218,7 +217,7 @@ class hsmUxInteraction:
def show(self):
from main import dis, hsm_active
from glob import dis, hsm_active
# Plan: show "time til period reset", and some stats,
# but never show amounts or private info.
@ -268,7 +267,7 @@ class hsmUxInteraction:
def draw_busy(self, msg, percent):
from display import FontTiny
from main import dis
from glob import dis
self.last_percent = 0.5
@ -306,21 +305,21 @@ class hsmUxInteraction:
self.draw_busy(None, percent)
async def interact(self):
import main
from main import numpad, is_devmode
import glob
from glob import numpad
from actions import login_now
from uasyncio import sleep_ms
# Replace some drawing functions
orig_fullscreen = main.dis.fullscreen
orig_progress_bar = main.dis.progress_bar
orig_progress_bar_show = main.dis.progress_bar_show
main.dis.fullscreen = self.hack_fullscreen
main.dis.progress_bar = self.hack_progress_bar
main.dis.progress_bar_show = self.hack_progress_bar
orig_fullscreen = glob.dis.fullscreen
orig_progress_bar = glob.dis.progress_bar
orig_progress_bar_show = glob.dis.progress_bar_show
glob.dis.fullscreen = self.hack_fullscreen
glob.dis.progress_bar = self.hack_progress_bar
glob.dis.progress_bar_show = self.hack_progress_bar
# get ready ourselves
main.dis.set_brightness(0) # dimest, but still readable
glob.dis.set_brightness(0) # dimest, but still readable
self.draw_background()
# Kill time, waiting for user input
@ -338,7 +337,7 @@ class hsmUxInteraction:
self.digits = ''
elif ch == 'y':
if len(self.digits) == LOCAL_PIN_LENGTH:
main.hsm_active.local_pin_entered(self.digits)
glob.hsm_active.local_pin_entered(self.digits)
self.digits = ''
elif ch == numpad.ABORT_KEY:
# important to eat these and fully suppress them
@ -349,7 +348,7 @@ class hsmUxInteraction:
self.digits += ch[0]
if len(self.digits) == LOCAL_PIN_LENGTH:
# send it, even if they didn't press OK yet
main.hsm_active.local_pin_entered(self.digits)
glob.hsm_active.local_pin_entered(self.digits)
# do immediate screen update
continue
@ -373,14 +372,14 @@ class hsmUxInteraction:
# and when the "boot_to_hsm" feature is used and successfully unlock near
# boottime.
from actions import goto_top_menu
main.hsm_active = None
glob.hsm_active = None
goto_top_menu()
# restore normal operation of UX
from display import Display
main.dis.fullscreen = orig_fullscreen
main.dis.progress_bar = orig_progress_bar
main.dis.progress_bar_show = orig_progress_bar_show
glob.dis.fullscreen = orig_fullscreen
glob.dis.progress_bar = orig_progress_bar
glob.dis.progress_bar_show = orig_progress_bar_show
return

45
shared/imptask.py Normal file
View File

@ -0,0 +1,45 @@
# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# imptask.py -- important async tasks that shouldn't die
#
import sys, uasyncio
class ImportantTask:
def __init__(self):
self.tasks = dict()
uasyncio.get_event_loop().set_exception_handler(self.handle_exc)
def handle_exc(self, loop, context):
# Unhandled exception in a task.
task = context['future']
# See if it matters: some tasks are short-lived and exception in them
# may not be fatal or even serious
for name, t in self.tasks.items():
if t == task:
print("Panic stop: %r has died" % name)
from main import die_with_debug
die_with_debug(context["exception"])
# not reached, except on simulator:
break
else:
print("IMPTASK: " + context["message"])
sys.print_exception(context["exception"])
print("... ignoring that")
def start_task(self, name, awaitable):
# start a critical task and watch for it to never die
task = uasyncio.create_task(awaitable)
self.tasks[name] = task
return task
def task_name(self, t):
for k, v in self.tasks.items():
if v == t:
return k
return None
IMPT = ImportantTask()
# EOF

View File

@ -1,16 +1,16 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# login.py - UX related to PIN code entry/login.
#
# NOTE: Mark3 hardware does not support secondary wallet concept.
#
import pincodes, version
from main import dis
import pincodes, version, random
from glob import dis
from display import FontLarge, FontTiny
from ux import PressRelease, ux_wait_keyup, ux_poll_once, ux_show_story
from utils import pretty_delay
from callgate import show_logout
from pincodes import pa
MAX_PIN_PART_LEN = 6
MIN_PIN_PART_LEN = 2
@ -26,9 +26,8 @@ class LoginUX:
self.randomize = randomize
def shuffle_keys(self):
from random import shuffle
keys = [str(i) for i in range(10)]
shuffle(keys)
random.shuffle(keys)
self.randomize = keys
def reset(self):
@ -211,7 +210,7 @@ class LoginUX:
self.show_pin()
async def do_delay(self, pa):
async def do_delay(self):
# show # of failures and implement the delay, which could be
# very long.
dis.clear()
@ -252,7 +251,6 @@ Press OK to continue, X to stop for now.
async def try_login(self, retry=True):
from main import pa
while retry:
if version.has_608 and not pa.attempts_left:
@ -280,7 +278,7 @@ Press OK to continue, X to stop for now.
await self.confirm_attempt(pa.attempts_left, pa.num_fails, pin)
elif pa.is_delay_needed():
# mark 1/2 might come here, never mark3
await self.do_delay(pa)
await self.do_delay()
# do the actual login attempt now
try:

View File

@ -1,14 +1,17 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# main.py
#
# - main.loop is imported and "run forever" by boot.py, forced into place by COLDCARD/initfs code
# - importing this file starts the system, see "go()"
# - NO: main.loop is imported and "run forever" by boot.py, forced into place by COLDCARD/initfs code
# - cannot be changed by /flash/lib overrides, because already imported before that.
#
# see RAM_HEADER_BASE, and coldcardFirmwareHeader_t in sigheader.h
import pyb, sys, version, gc
import pyb, sys, gc, glob
from imptask import IMPT
assert not glob.dis, "main reimport"
# this makes the GC run when larger objects are free in an attempt to reduce fragmentation.
gc.threshold(4096)
@ -21,10 +24,18 @@ if 0:
pyb.usb_mode('VCP')
raise SystemExit
# what firmware signing key did we boot with? are we in dev mode?
is_devmode = version.is_devmode()
print("---\nColdcard Wallet from Coinkite Inc. (c) 2018-2021.\n")
if is_devmode:
# Setup OLED and get something onto it.
from display import Display
dis = Display()
dis.splash()
glob.dis = dis
# slowish imports, some with side-effects
import version, ckcc, uasyncio
if version.is_devmode:
# For devs only: allow code in this directory to overide compiled-in stuff. Dangerous!
# - using relative paths here so works better on simulator
# - you must boot w/ non-production-signed firmware to get here
@ -35,43 +46,41 @@ if is_devmode:
import boot2
except: pass
import ckcc
import uasyncio.core as asyncio
loop = asyncio.get_event_loop()
print("---\nColdcard Wallet from Coinkite Inc. (c) 2020.\n")
# Setup OLED and get something onto it.
from display import Display
dis = Display()
dis.splash()
# Setup membrane numpad (mark 2+)
from mempad import MembraneNumpad
numpad = MembraneNumpad(loop)
numpad = MembraneNumpad()
glob.numpad = numpad
# Serial Flash memory
from sflash import SPIFlash
sf = SPIFlash()
from sflash import SF
# NV settings
from nvstore import SettingsObject
settings = SettingsObject(loop)
from nvstore import settings
# global ptr to HSM policy, if any (supported on Mk3 only)
hsm_active = None
async def done_splash2():
# Boot up code; after splash screen is done.
async def more_setup():
# Boot up code; splash screen is being shown
# MAYBE: check if we're a brick and die again? Or show msg?
# Some background "tasks"
#
from dev_helper import monitor_usb
IMPT.start_task('vcp', monitor_usb())
from files import CardSlot
CardSlot.setup()
# This "pa" object holds some state shared w/ bootloader about the PIN
try:
from pincodes import pa
pa.setup(b'') # just to see where we stand.
except RuntimeError as e:
print("Problem: %r" % e)
if version.is_factory_mode:
# in factory mode, turn on USB early to allow debug/setup
from usb import enable_usb
enable_usb(loop, True)
enable_usb()
# always start the self test.
if not settings.get('tested', False):
@ -89,80 +98,71 @@ async def done_splash2():
from actions import start_login_sequence
await start_login_sequence()
loop.create_task(mainline())
IMPT.start_task('mainline', mainline())
async def mainline():
# Mainline of program, after startup
#
# - Do not add to this function, its vars are
# in memory forever; instead, extend done_splash2 above.
# in memory forever; instead, extend more_setup above.
from actions import goto_top_menu
from ux import the_ux
goto_top_menu()
gc.collect()
#print("Free mem: %d" % gc.mem_free())
while 1:
await the_ux.interact()
# Setup to start the splash screen.
dis.splash_animate(loop, done_splash2)
def die_with_debug(exc):
from usb import is_vcp_active
is_debug = is_vcp_active()
# Some background "tasks"
#
from dev_helper import monitor_usb
loop.create_task(monitor_usb())
if is_debug and isinstance(exc, KeyboardInterrupt):
# preserve GUI state, but want to see where we are
print("KeyboardInterrupt")
raise exc
elif isinstance(exc, SystemExit):
# Ctrl-D and warm reboot cause this, not bugs
raise exc
else:
# show stacktrace for debug photos
try:
import uio, ux
tmp = uio.StringIO()
sys.print_exception(exc, tmp)
msg = tmp.getvalue()
del tmp
print(msg)
ux.show_fatal_error(msg)
except: pass
from files import CardSlot
CardSlot.setup()
# securely die (wipe memory)
if not is_debug:
try:
import callgate
callgate.show_logout(1)
except: pass
# This "pa" object holds some state shared w/ bootloader about the PIN
try:
from pincodes import PinAttempt
pa = PinAttempt()
pa.setup(b'') # just to see where we stand.
except RuntimeError as e:
print("Problem: %r" % e)
def go():
# Wrapper for better error handling/recovery at top level.
#
try:
loop.run_forever()
uasyncio.get_event_loop().run_forever()
raise RuntimeError('main.stop') # not expected
except BaseException as exc:
from usb import is_vcp_active
is_debug = is_vcp_active()
die_with_debug(exc)
if is_debug and isinstance(exc, KeyboardInterrupt):
# preserve GUI state, but want to see where we are
print("KeyboardInterrupt")
raise
elif isinstance(exc, SystemExit):
# Ctrl-D and warm reboot cause this, not bugs
raise
else:
# show stacktrace for debug photos
try:
import uio, ux
tmp = uio.StringIO()
sys.print_exception(exc, tmp)
msg = tmp.getvalue()
del tmp
print(msg)
ux.show_fatal_error(msg)
except: pass
# securely die (wipe memory)
if not is_debug:
try:
import callgate
callgate.show_logout(1)
except: pass
if is_devmode:
if version.is_devmode:
# Give external devs a way to start semi-early.
try:
import main2
except: pass
uasyncio.create_task(more_setup())
go()
# EOF

60
shared/manifest.py Normal file
View File

@ -0,0 +1,60 @@
# freeze everything in this directoy
freeze_as_mpy('', [
'actions.py',
'address_explorer.py',
'auth.py',
'backups.py',
'callgate.py',
'chains.py',
'choosers.py',
'compat7z.py',
'descriptor.py',
'dev_helper.py',
'display.py',
'drv_entro.py',
'exceptions.py',
'export.py',
'files.py',
'flow.py',
'glob.py',
'h.py',
'history.py',
'hsm.py',
'hsm_ux.py',
'imptask.py',
'login.py',
'main.py',
'mempad.py',
'menu.py',
'multisig.py',
'numpad.py',
'nvstore.py',
'opcodes.py',
'paper.py',
'pincodes.py',
'psbt.py',
'pwsave.py',
'random.py',
'seed.py',
'selftest.py',
'serializations.py',
'sffile.py',
'sflash.py',
'sram2.py',
'ssd1306.py',
'stash.py',
'usb.py',
'users.py',
'utils.py',
'ux.py',
'version.py',
'queues.py',
], opt=0)
# Data-like files, since no need to debug them
freeze_as_mpy('', [
'sigheader.py',
'graphics.py',
'zevvpeep.py',
'public_constants.py',
], opt=3)

View File

@ -1,14 +1,13 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# mempad.py - Numeric keypad implemented with membrane metal-dome, not touch.
#
import array, utime, pyb
from uasyncio.queues import Queue
from ucollections import deque
from machine import Pin
from random import shuffle
from numpad import NumpadBase
from utils import call_later_ms
NUM_ROWS = const(4)
NUM_COLS = const(3)
@ -21,8 +20,8 @@ DECODER = 'y0x987654321'
class MembraneNumpad(NumpadBase):
def __init__(self, loop):
super(MembraneNumpad, self).__init__(loop)
def __init__(self):
super(MembraneNumpad, self).__init__()
# No idea how to pick a safe timer number.
self.timer = pyb.Timer(7)
@ -55,7 +54,6 @@ class MembraneNumpad(NumpadBase):
c.irq(self.anypress_irq, Pin.IRQ_FALLING|Pin.IRQ_RISING)
# ready to start
self.loop = loop
def anypress_irq(self, pin):
# come here for any change, high or low
@ -85,7 +83,7 @@ class MembraneNumpad(NumpadBase):
self._history = bytearray(NUM_ROWS * NUM_COLS)
self.timer.init(freq=SAMPLE_FREQ, callback=self._measure_irq)
self.loop.call_later_ms(Q_CHECK_RATE, self._finish_scan)
call_later_ms(Q_CHECK_RATE, self._finish_scan)
def _measure_irq(self, _timer):
# CHALLENGE: Called at high rate, and cannot do memory alloc.
@ -135,12 +133,9 @@ class MembraneNumpad(NumpadBase):
self._history[i] = 0
def _finish_scan(self):
async def _finish_scan(self):
# we're done a full scan (mulitple times: NUM_SAMPLES)
# - not trying to support multiple presses, just one
from main import dis
from display import FontTiny
while self.scans:
event = self.scans.popleft()
@ -159,6 +154,6 @@ class MembraneNumpad(NumpadBase):
# stop scanning now... nothing happening
self._wait_any()
else:
self.loop.call_later_ms(Q_CHECK_RATE, self._finish_scan)
call_later_ms(Q_CHECK_RATE, self._finish_scan)
# EOF

View File

@ -1,16 +1,17 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# menu.py - Implement an interactive menu system.
#
import gc
from main import dis
from display import FontLarge, FontTiny
from ux import PressRelease, the_ux
from uasyncio import sleep_ms
# number of full lines per screen
PER_M = 4
PER_M = const(4)
# do wrap-around, but only for mega menus like seed words
WRAP_IF_OVER = const(16)
def start_chooser(chooser):
# get which one to show as selected, list of choices, and fcn to call after
@ -100,6 +101,7 @@ class MenuSystem:
#
# Redraw the menu.
#
from glob import dis
dis.clear()
#print('cur=%d ypos=%d' % (self.cursor, self.ypos))
@ -140,14 +142,19 @@ class MenuSystem:
def down(self):
if self.cursor < self.count-1:
self.cursor += 1
if self.cursor - self.ypos >= (PER_M-1):
self.ypos += 1
if self.cursor - self.ypos >= (PER_M-1):
self.ypos += 1
elif self.count > WRAP_IF_OVER:
self.goto_idx(0)
def up(self):
if self.cursor > 0:
self.cursor -= 1
if self.cursor < self.ypos:
self.ypos -= 1
elif self.count > WRAP_IF_OVER:
self.goto_idx(self.count-1)
def top(self):
self.cursor = 0

View File

@ -1,9 +1,8 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# multisig.py - support code for multisig signing and p2sh in general.
#
import stash, chains, ustruct, ure, uio, sys, tcc
import stash, chains, ustruct, ure, uio, sys, ngu
#from ubinascii import hexlify as b2a_hex
from utils import xfp2str, str2xfp, swab32, cleanup_deriv_path, keypath_to_str, str_to_keypath
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, ux_clear_keys
@ -13,6 +12,7 @@ from menu import MenuSystem, MenuItem
from opcodes import OP_CHECKMULTISIG
from actions import needs_microsd
from exceptions import FatalPSBTIssue
from nvstore import settings
# Bitcoin limitation: max number of signatures in CHECK_MULTISIG
# - 520 byte redeem script limit <= 15*34 bytes per pubkey == 510 bytes
@ -93,10 +93,11 @@ def make_redeem_script(M, nodes, subkey_idx):
pubkeys = []
for n in nodes:
copy = n.clone()
copy.derive(subkey_idx)
copy = n.copy()
copy.derive(subkey_idx, False)
# 0x21 = 33 = len(pubkey) = OP_PUSHDATA(33)
pubkeys.append(b'\x21' + copy.public_key())
pubkeys.append(b'\x21' + copy.pubkey())
del copy
pubkeys.sort()
@ -158,7 +159,6 @@ class MultisigWallet:
@classmethod
def get_trust_policy(cls):
from main import settings
which = settings.get('pms', None)
@ -221,7 +221,6 @@ class MultisigWallet:
def iter_wallets(cls, M=None, N=None, not_idx=None, addr_fmt=None):
# yield MS wallets we know about, that match at least right M,N if known.
# - this is only place we should be searching this list, please!!
from main import settings
lst = settings.get('multisig', [])
for idx, rec in enumerate(lst):
@ -327,13 +326,11 @@ class MultisigWallet:
@classmethod
def exists(cls):
# are there any wallets defined?
from main import settings
return bool(settings.get('multisig', False))
@classmethod
def get_by_idx(cls, nth):
# instance from index number (used in menu)
from main import settings
lst = settings.get('multisig', [])
try:
obj = lst[nth]
@ -345,8 +342,6 @@ class MultisigWallet:
def commit(self):
# data to save
# - important that this fails immediately when nvram overflows
from main import settings
obj = self.serialize()
v = settings.get('multisig', [])
@ -416,8 +411,6 @@ class MultisigWallet:
def delete(self):
# remove saved entry
# - important: not expecting more than one instance of this class in memory
from main import settings
assert self.storage_idx >= 0
# safety check
@ -453,7 +446,7 @@ class MultisigWallet:
for xfp, deriv, xpub in self.xpubs:
# load bip32 node for each cosigner, derive /0/ based on change idx
node = ch.deserialize_node(xpub, AF_P2SH)
node.derive(change_idx)
node.derive(change_idx, False)
nodes.append(node)
# indicate path used (for UX)
@ -493,10 +486,10 @@ class MultisigWallet:
for pk_order, pubkey in enumerate(pubkeys):
check_these = []
# TODO: this could be simpler now that XFP is unique per co-signer
if subpaths:
# in PSBT, we are given a map from pubkey to xfp/path, use it
# while remembering it's potentially one-2-many
# TODO: this could be simpler now
assert pubkey in subpaths, "unexpected pubkey"
xfp, *path = subpaths[pubkey]
@ -535,9 +528,9 @@ class MultisigWallet:
for sp in path[dp:]:
assert not (sp & 0x80000000), 'hard deriv'
node.derive(sp) # works in-place
node.derive(sp, False) # works in-place
found_pk = node.public_key()
found_pk = node.pubkey()
# Document path(s) used. Not sure this is useful info to user tho.
# - Do not show what we can't verify: we don't really know the hardeneded
@ -592,8 +585,6 @@ class MultisigWallet:
# - M of N line (assume N of N if not spec'd)
# - xpub: any bip32 serialization we understand, but be consistent
#
from main import settings
my_xfp = settings.get('xfp')
deriv = None
xpubs = []
@ -716,7 +707,10 @@ class MultisigWallet:
except:
raise AssertionError('unable to parse xpub')
assert node.private_key() == None # 'no privkeys plz'
try:
assert node.privkey() == None # 'no privkeys plz'
except ValueError:
pass
assert chain.ctype == expect_chain # 'wrong chain'
depth = node.depth()
@ -724,11 +718,11 @@ class MultisigWallet:
if depth == 1:
if not xfp:
# allow a shortcut: zero/omit xfp => use observed parent value
xfp = swab32(node.fingerprint())
xfp = swab32(node.parent_fp())
else:
# generally cannot check fingerprint values, but if we can, do so.
if not cls.disable_checks:
assert swab32(node.fingerprint()) == xfp, 'xfp depth=1 wrong'
assert swab32(node.parent_fp()) == xfp, 'xfp depth=1 wrong'
assert xfp, 'need fingerprint' # happens if bare xpub given
@ -736,7 +730,9 @@ class MultisigWallet:
# and we know none of the private keys involved.
if depth == 1:
# but derivation is implied at depth==1
guess = keypath_to_str([node.child_num()], skip=0)
kn, is_hard = node.child_number()
if is_hard: kn |= 0x80000000
guess = keypath_to_str([kn], skip=0)
if deriv:
if not cls.disable_checks:
@ -759,7 +755,7 @@ class MultisigWallet:
# and that's not supported
with stash.SensitiveValues() as sv:
chk_node = sv.derive_path(deriv)
assert node.public_key() == chk_node.public_key(), \
assert node.pubkey() == chk_node.pubkey(), \
"(m=%s)/%s wrong pubkey" % (xfp2str(xfp), deriv[2:])
# serialize xpub w/ BIP32 standard now.
@ -806,7 +802,6 @@ class MultisigWallet:
async def export_wallet_file(self, mode="exported from", extra_msg=None):
# create a text file with the details; ready for import to next Coldcard
from main import settings
my_xfp = xfp2str(settings.get('xfp'))
fname_pattern = self.make_fname('export')
@ -882,8 +877,6 @@ class MultisigWallet:
# the details, and/or bypass that all and just trust the data.
# - xpubs_list is a list of (xfp+path, binary BIP32 xpub)
# - already know not in our records.
from main import settings
trust_mode = cls.get_trust_policy()
if trust_mode == TRUST_VERIFY:
@ -903,7 +896,7 @@ class MultisigWallet:
for k, v in xpubs_list:
xfp, *path = ustruct.unpack_from('<%dI' % (len(k)//4), k, 0)
xpub = tcc.codecs.b58_encode(v)
xpub = ngu.codecs.b58_encode(v)
is_mine = cls.check_xpub(xfp, xpub, keypath_to_str(path, skip=0),
expect_chain, my_xfp, xpubs)
if is_mine:
@ -933,7 +926,7 @@ class MultisigWallet:
for k, v in xpubs_list:
xfp, *path = ustruct.unpack_from('<%dI' % (len(k)//4), k, 0)
xpub = tcc.codecs.b58_encode(v)
xpub = ngu.codecs.b58_encode(v)
# cleanup and normalize xpub
tmp = []
@ -1110,7 +1103,6 @@ def psbt_xpubs_policy_chooser():
ch = [ 'Verify Only', 'Offer Import', 'Trust PSBT']
def xset(idx, text):
from main import settings
settings.set('pms', idx)
return MultisigWallet.get_trust_policy(), ch, xset
@ -1175,7 +1167,7 @@ class MultisigMenu(MenuSystem):
async def make_multisig_menu(*a):
# list of all multisig wallets, and high-level settings/actions
from main import pa
from pincodes import pa
if pa.is_secret_blank():
await ux_show_story("You must have wallet seed before creating multisig wallets.")
@ -1265,7 +1257,6 @@ async def export_multisig_xpubs(*a):
#
# Consumer for this file is supposed to be ourselves, when we build on-device multisig.
#
from main import settings
xfp = xfp2str(settings.get('xfp', 0))
chain = chains.current_chain()
@ -1335,21 +1326,13 @@ def import_xpub(ln):
if not found:
return None
found = found.group(0)
# serialize, and note version code
try:
node, chain, addr_fmt, _ = chains.slip32_deserialize(found.group(0))
except:
return None
for ch in chains.AllChains:
for kk in ch.slip132:
if found[0] == ch.slip132[kk].hint:
try:
node = tcc.bip32.deserialize(found, ch.slip132[kk].pub, ch.slip132[kk].priv)
chain = ch
addr_fmt = kk
return (node, ch, kk)
except ValueError:
pass
# looked like one, but fail.
return None
return (node, chain, addr_fmt)
async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH):
# collect all xpub- exports on current SD card (must be > 1)
@ -1360,7 +1343,6 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH):
from actions import file_picker
import uos, ujson
from utils import get_filesize
from main import settings
chain = chains.current_chain()
my_xfp = settings.get('xfp')

View File

@ -1,10 +1,9 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# numpad.py - Base class for numeric keypads. Touch or membrane matrix.
#
import utime
from uasyncio.queues import Queue
from queues import Queue
class NumpadBase:
@ -13,7 +12,7 @@ class NumpadBase:
# this signals a need to stop user interaction and re-look at ux stack
ABORT_KEY = '\xff'
def __init__(self, loop):
def __init__(self):
# once pressed, and released; keys show up in this queue
self._changes = Queue(24)
self.key_pressed = ''

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# nvstore.py - manage a few key values that aren't super secrets
#
@ -18,10 +17,13 @@
# - you cannot move data between slots because AES-CTR with CTR seed based on slot #
# - SHA check on decrypted data
#
import os, ujson, tcc, ustruct, ckcc, gc
from uasyncio import sleep_ms
import os, ujson, ustruct, ckcc, gc, ngu, aes256ctr
from uio import BytesIO
from sffile import SFFile
from sflash import SF
from uhashlib import sha256
from random import shuffle
from utils import call_later_ms
# Setting values:
# xfp = master xpub's fingerprint (32 bit unsigned)
@ -59,8 +61,7 @@ _tmp = nvstore_buf
class SettingsObject:
def __init__(self, loop=None):
self.loop = loop
def __init__(self, dis=None):
self.is_dirty = 0
self.my_pos = 0
@ -69,18 +70,19 @@ class SettingsObject:
self.current = self.default_values()
self.overrides = {} # volatile overide values
self.load()
self.load(dis)
def get_aes(self, mode, pos):
# Build AES key for en/decrypt of specific block.
def get_aes(self, pos):
# Build AES object for en/decrypt of specific block.
# Include the slot number as part of the initial counter (CTR)
return tcc.AES(tcc.AES.CTR | mode, self.nvram_key, ustruct.pack('<4I', 4, 3, 2, pos))
ctr = ustruct.pack('<4I', 4, 3, 2, pos)
return aes256ctr.new(self.nvram_key, ctr)
def set_key(self, new_secret=None):
# System settings (not secrets) are stored in SPI Flash, encrypted with this
# key that is derived from main wallet secret. Call this method when the secret
# is first loaded, or changes for some reason.
from main import pa
from pincodes import pa
from stash import blank_object
key = None
@ -99,12 +101,12 @@ class SettingsObject:
# hash up the secret... without decoding it or similar
assert len(new_secret) >= 32
s = tcc.sha256(new_secret)
s = sha256(new_secret)
for round in range(5):
s.update('pad')
s = tcc.sha256(s.digest())
s = sha256(s.digest())
key = s.digest()
@ -114,11 +116,9 @@ class SettingsObject:
# for restore from backup case, or when changing (created) the seed
self.nvram_key = key
def load(self):
def load(self, dis=None):
# Search all slots for any we can read, decrypt that,
# and pick the newest one (in unlikely case of dups)
from main import sf
# reset
self.current.clear()
self.overrides.clear()
@ -132,31 +132,32 @@ class SettingsObject:
buf = bytearray(4)
empty = 0
for pos in SLOTS:
if dis:
dis.progress_bar_show((pos-SLOTS.start) / (SLOTS.stop-SLOTS.start))
gc.collect()
sf.read(pos, buf)
SF.read(pos, buf)
if buf[0] == buf[1] == buf[2] == buf[3] == 0xff:
# erased (probably)
empty += 1
continue
# check if first 2 bytes makes sense for JSON
aes = self.get_aes(tcc.AES.Encrypt, pos)
chk = aes.update(b'{"')
aes = self.get_aes(pos)
chk = aes.copy().cipher(b'{"')
if chk != buf[0:2]:
# doesn't look like JSON meant for me
continue
# probably good, read it
aes = self.get_aes(tcc.AES.Encrypt, pos)
chk = tcc.sha256()
chk = sha256()
aes = aes.cipher
expect = None
with SFFile(pos, length=4096, pre_erased=True) as fd:
for i in range(4096/32):
b = aes.update(fd.read(32))
b = aes(fd.read(32))
if i != 127:
_tmp[i*32:(i*32)+32] = b
chk.update(b)
@ -187,8 +188,8 @@ class SettingsObject:
# stale data seen; clean it up.
assert self.current['_age'] > 0
#print("NV: cleanup @ %d" % pos)
sf.sector_erase(pos)
sf.wait_done()
SF.sector_erase(pos)
SF.wait_done()
# 4k is a large object, sigh, for us right now. cleanup
gc.collect()
@ -205,13 +206,13 @@ class SettingsObject:
# Whole thing is blank. Bad for plausible deniability. Write 3 slots
# with garbage. They will be wasted space until it fills.
blks = list(SLOTS)
tcc.random.shuffle(blks)
shuffle(blks)
for pos in blks[0:3]:
for i in range(0, 4096, 256):
h = tcc.random.bytes(256)
sf.wait_done()
sf.write(pos+i, h)
h = ngu.random.bytes(256)
SF.wait_done()
SF.write(pos+i, h)
def get(self, kn, default=None):
if kn in self.overrides:
@ -221,8 +222,8 @@ class SettingsObject:
def changed(self):
self.is_dirty += 1
if self.is_dirty < 2 and self.loop:
self.loop.call_later_ms(250, self.write_out())
if self.is_dirty < 2:
call_later_ms(250, self.write_out)
def put(self, kn, v):
self.current[kn] = v
@ -259,46 +260,44 @@ class SettingsObject:
gc.collect()
self.save()
except MemoryError:
self.loop.call_later_ms(250, self.write_out())
call_later_ms(250, self.write_out)
def find_spot(self, not_here=0):
# search for a blank sector to use
# - check randomly and pick first blank one (wear leveling, deniability)
# - we will write and then erase old slot
# - if "full", blow away a random one
from main import sf
options = [s for s in SLOTS if s != not_here]
tcc.random.shuffle(options)
shuffle(options)
buf = bytearray(16)
for pos in options:
sf.read(pos, buf)
SF.read(pos, buf)
if set(buf) == {0xff}:
# blank
return sf, pos
return pos
# No where to write! (probably a bug because we have lots of slots)
# ... so pick a random slot and kill what it had
#print("ERROR: nvram full?")
victem = options[0]
sf.sector_erase(victem)
sf.wait_done()
SF.sector_erase(victem)
SF.wait_done()
return sf, victem
return victem
def save(self):
# render as JSON, encrypt and write it.
self.current['_age'] = self.current.get('_age', 1) + 1
sf, pos = self.find_spot(self.my_pos)
pos = self.find_spot(self.my_pos)
aes = self.get_aes(tcc.AES.Encrypt, pos)
aes = self.get_aes(pos).cipher
with SFFile(pos, max_size=4096, pre_erased=True) as fd:
chk = tcc.sha256()
chk = sha256()
# first the json data
d = ujson.dumps(self.current)
@ -310,7 +309,7 @@ class SettingsObject:
self.capacity = dat_len / 4096
fd.write(aes.update(d))
fd.write(aes(d))
chk.update(d)
del d
@ -318,19 +317,19 @@ class SettingsObject:
here = min(32, pad_len)
pad = bytes(here)
fd.write(aes.update(pad))
fd.write(aes(pad))
chk.update(pad)
pad_len -= here
fd.write(aes.update(chk.digest()))
fd.write(aes(chk.digest()))
assert fd.tell() == 4096
# erase old copy of data
if self.my_pos and self.my_pos != pos:
sf.wait_done()
sf.sector_erase(self.my_pos)
sf.wait_done()
SF.wait_done()
SF.sector_erase(self.my_pos)
SF.wait_done()
self.my_pos = pos
self.is_dirty = 0
@ -342,11 +341,9 @@ class SettingsObject:
def blank(self):
# erase current copy of values in nvram; older ones may exist still
# - use when clearing the seed value
from main import sf
if self.my_pos:
sf.wait_done()
sf.sector_erase(self.my_pos)
SF.wait_done()
SF.sector_erase(self.my_pos)
self.my_pos = 0
# act blank too, just in case.
@ -361,4 +358,8 @@ class SettingsObject:
# where value is used, and treat undefined as the default state.
return dict(_age=0)
# not a singleton, but default widely-used object
from glob import dis
settings = SettingsObject(dis)
# EOF

View File

@ -83,24 +83,26 @@ class PaperWalletMaker:
async def doit(self, *a, have_key=None):
# make the wallet.
from main import dis
from glob import dis
try:
import ngu
from chains import current_chain
import tcc
from serializations import hash160
from stash import blank_object
if not have_key:
# get some random bytes
await ux_dramatic_pause("Picking key...", 2)
privkey = tcc.secp256k1.generate_secret()
pair = ngu.secp256k1.keypair()
else:
# caller must range check this already: 0 < privkey < order
privkey = have_key
# - actually libsecp256k1 will check it again anyway
pair = ngu.secp256k1.keypair(have_key)
# calculate corresponding public key value
pubkey = tcc.secp256k1.publickey(privkey, True) # always compressed style
# pull out binary versions (serialized) as we need
privkey = pair.privkey()
pubkey = pair.pubkey().to_bytes(False) # always compressed style
dis.fullscreen("Rendering...")
@ -108,11 +110,11 @@ class PaperWalletMaker:
digest = hash160(pubkey)
ch = current_chain()
if self.is_segwit:
addr = tcc.codecs.bech32_encode(ch.bech32_hrp, 0, digest)
addr = ngu.codecs.segwit_encode(ch.bech32_hrp, 0, digest)
else:
addr = tcc.codecs.b58_encode(ch.b58_addr + digest)
addr = ngu.codecs.b58_encode(ch.b58_addr + digest)
wif = tcc.codecs.b58_encode(ch.b58_privkey + privkey + b'\x01')
wif = ngu.codecs.b58_encode(ch.b58_privkey + privkey + b'\x01')
if self.can_do_qr():
with imported('uqr') as uqr:
@ -205,7 +207,7 @@ class PaperWalletMaker:
w = qr.width()
for y in range(w):
fp.write(' ')
ln = ''.join('\u2588\u2588' if qr.get(x,y) else '' for x in range(w))
ln = ''.join('\u2588\u2588' if qr.get(x,y) else ' ' for x in range(w))
fp.write(ln)
fp.write('\n')

View File

@ -1,11 +1,11 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# pincodes.py - manage PIN code (which map to wallet seeds)
#
import ustruct, ckcc, tcc, version
import ustruct, ckcc, version
from ubinascii import hexlify as b2a_hex
from callgate import enter_dfu
from bip39 import wordlist_en
# See ../stm32/bootloader/pins.h for source of these constants.
#
@ -275,7 +275,7 @@ class PinAttempt:
w1 = (bits >> 11) & 0x7ff
w2 = bits & 0x7ff
rv = tcc.bip39.lookup_nth(w1), tcc.bip39.lookup_nth(w2)
rv = wordlist_en[w1], wordlist_en[w2]
if version.has_608:
# MRU: keep only a few
@ -377,7 +377,7 @@ class PinAttempt:
def new_main_secret(self, raw_secret, chain=None):
# Main secret has changed: reset the settings+their key,
# and capture xfp/xpub
from main import settings
from nvstore import settings
import stash
# capture values we have already
@ -397,5 +397,7 @@ class PinAttempt:
# does not call settings.save() but caller should!
# singleton
pa = PinAttempt()
# EOF

View File

@ -1,12 +1,12 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# psbt.py - understand PSBT file format: verify and generate them
#
from ustruct import unpack_from, unpack, pack
from ubinascii import hexlify as b2a_hex
from utils import xfp2str, B2A, keypath_to_str, problem_file_line
import tcc, stash, gc, history, sys
import stash, gc, history, sys, ngu
from uhashlib import sha256
from uio import BytesIO
from sffile import SizerFile
from sram2 import psbt_tmp256
@ -16,6 +16,7 @@ from serializations import ser_compact_size, deser_compact_size, hash160, hash25
from serializations import CTxIn, CTxInWitness, CTxOut, SIGHASH_ALL, ser_uint256
from serializations import ser_sig_der, uint256_from_str, ser_push_data, uint256_from_str
from serializations import ser_string
from nvstore import settings
from public_constants import (
PSBT_GLOBAL_UNSIGNED_TX, PSBT_GLOBAL_XPUB, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO,
@ -34,7 +35,7 @@ DEBUG = const(0)
class HashNDump:
def __init__(self, d=None):
self.rv = tcc.sha256()
self.rv = sha256()
print('Hashing: ', end='')
if d:
self.update(d)
@ -104,7 +105,7 @@ def calc_txid(fd, poslen, body_poslen=None):
# txn does not have witness data, so txid==wtxix
return get_hash256(fd, poslen)
rv = tcc.sha256()
rv = sha256()
# de/reserialize much of the txn -- but not the witness data
rv.update(pack("<i", txn_version))
@ -128,12 +129,12 @@ def calc_txid(fd, poslen, body_poslen=None):
rv.update(fd.read(4))
return tcc.sha256(rv.digest()).digest()
return ngu.hash.sha256s(rv.digest())
def get_hash256(fd, poslen, hasher=None):
# return the double-sha256 of a value, without loading it into memory
pos, ll = poslen
rv = hasher or tcc.sha256()
rv = hasher or sha256()
fd.seek(pos)
while ll:
@ -147,7 +148,7 @@ def get_hash256(fd, poslen, hasher=None):
if hasher:
return
return tcc.sha256(rv.digest()).digest()
return ngu.hash.sha256s(rv.digest())
class psbtProxy:
@ -425,7 +426,7 @@ class psbtOutputProxy(psbtProxy):
# p2wsh case
# - need witness script and check it's hash against proposed p2wsh value
assert len(addr_or_pubkey) == 32
expect_wsh = tcc.sha256(witness_script).digest()
expect_wsh = ngu.hash.sha256s(witness_script)
if expect_wsh != addr_or_pubkey:
raise FraudulentChangeOutput(out_idx, "P2WSH witness script has wrong hash")
@ -434,7 +435,7 @@ class psbtOutputProxy(psbtProxy):
if witness_script:
# p2sh-p2wsh case (because it had witness script)
expect_rs = b'\x00\x20' + tcc.sha256(witness_script).digest()
expect_rs = b'\x00\x20' + ngu.hash.sha256s(witness_script)
if redeem_script and expect_rs != redeem_script:
# iff they provide a redeeem script, then it needs to match
@ -838,7 +839,7 @@ class psbtObject(psbtProxy):
self.txn = None
self.xpubs = [] # tuples(xfp_path, xpub)
from main import settings, dis
from glob import dis
self.my_xfp = settings.get('xfp', 0)
# details that we discover as we go
@ -1067,7 +1068,7 @@ class psbtObject(psbtProxy):
proposed, need_approval = MultisigWallet.import_from_psbt(M, N, self.xpubs)
if need_approval:
# do a complex UX sequence, which lets them save new wallet
from main import hsm_active
from glob import hsm_active
if hsm_active:
raise FatalPSBTIssue("MS enroll not allowed in HSM mode")
@ -1138,7 +1139,6 @@ class psbtObject(psbtProxy):
else:
per_fee = self.calculate_fee() * 100 / self.total_value_out
from main import settings
fee_limit = settings.get('fee_limit', DEFAULT_MAX_FEE_PERCENTAGE)
if fee_limit != -1 and per_fee >= fee_limit:
@ -1399,7 +1399,7 @@ class psbtObject(psbtProxy):
# - inputs might be p2sh, p2pkh and/or segwit style
# - save partial inputs somewhere (append?)
# - update our state with new partial sigs
from main import dis
from glob import dis
with stash.SensitiveValues() as sv:
# Double check the change outputs are right. This is slow, but critical because
@ -1427,7 +1427,7 @@ class psbtObject(psbtProxy):
node = sv.derive_path(skp)
# check the pubkey of this BIP32 node
if pubkey == node.public_key():
if pubkey == node.pubkey():
good += 1
if not good:
@ -1478,7 +1478,7 @@ class psbtObject(psbtProxy):
node = sv.derive_path(skp, register=False)
# expensive test, but works... and important
pu = node.public_key()
pu = node.pubkey()
if pu == which_key:
break
else:
@ -1501,18 +1501,18 @@ class psbtObject(psbtProxy):
node = sv.derive_path(skp, register=False)
# expensive test, but works... and important
pu = node.public_key()
pu = node.pubkey()
assert pu == which_key, "Path (%s) led to wrong pubkey for input#%d"%(skp, in_idx)
# The precious private key we need
pk = node.private_key()
pk = node.privkey()
#print("privkey %s" % b2a_hex(pk).decode('ascii'))
#print(" pubkey %s" % b2a_hex(which_key).decode('ascii'))
#print(" digest %s" % b2a_hex(digest).decode('ascii'))
# Do the ACTUAL signature ... finally!!!
result = tcc.secp256k1.sign(pk, digest)
result = ngu.secp256k1.sign(pk, digest).to_bytes()
# private key no longer required
stash.blank_object(pk)
@ -1548,7 +1548,7 @@ class psbtObject(psbtProxy):
# - sha256 over that
fd = self.fd
old_pos = fd.tell()
rv = tcc.sha256()
rv = sha256()
# version number
rv.update(pack('<i', self.txn_version)) # nVersion
@ -1581,7 +1581,7 @@ class psbtObject(psbtProxy):
fd.seek(old_pos)
# double SHA256
return tcc.sha256(rv.digest()).digest()
return ngu.hash.sha256s(rv.digest())
def make_txn_segwit_sighash(self, replace_idx, replacement, amount, scriptCode, sighash_type):
# Implement BIP 143 hashing algo for signature of segwit programs.
@ -1596,25 +1596,25 @@ class psbtObject(psbtProxy):
if self.hashPrevouts is None:
# First time thru, we'll need to hash up this stuff.
po = tcc.sha256()
sq = tcc.sha256()
po = sha256()
sq = sha256()
# input side
for in_idx, txi in self.input_iter():
po.update(txi.prevout.serialize())
sq.update(pack("<I", txi.nSequence))
self.hashPrevouts = tcc.sha256(po.digest()).digest()
self.hashSequence = tcc.sha256(sq.digest()).digest()
self.hashPrevouts = ngu.hash.sha256s(po.digest())
self.hashSequence = ngu.hash.sha256s(sq.digest())
del po, sq, txi
# output side
ho = tcc.sha256()
ho = sha256()
for out_idx, txo in self.output_iter():
ho.update(txo.serialize())
self.hashOutputs = tcc.sha256(ho.digest()).digest()
self.hashOutputs = ngu.hash.sha256s(ho.digest())
del ho, txo
gc.collect()
@ -1623,7 +1623,7 @@ class psbtObject(psbtProxy):
#print('hSeq : %s' % str(b2a_hex(self.hashSequence), 'ascii'))
#print('hOuts: %s' % str(b2a_hex(self.hashOutputs), 'ascii'))
rv = tcc.sha256()
rv = sha256()
# version number
rv.update(pack('<i', self.txn_version)) # nVersion
@ -1647,7 +1647,7 @@ class psbtObject(psbtProxy):
fd.seek(old_pos)
# double SHA256
return tcc.sha256(rv.digest()).digest()
return ngu.hash.sha256s(rv.digest())
def is_complete(self):
# Are all the inputs (now) signed?
@ -1750,7 +1750,7 @@ class psbtObject(psbtProxy):
# calc transaction ID
if not needs_witness:
# easy w/o witness data
txid = tcc.sha256(fd.checksum.digest()).digest()
txid = ngu.hash.sha256s(fd.checksum.digest())
else:
# legacy cost here for segwit: re-read what we just wrote
txid = calc_txid(fd, (0, fd.tell()), (body_start, body_end-body_start))

View File

@ -1,9 +1,8 @@
# (c) Copyright 2020 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# pwsave.py - Save bip39 passphrases into encrypted file on MicroSD (if desired)
#
import sys, tcc, stash, ujson, os
import sys, stash, ujson, os, ngu
from files import CardSlot, CardMissingError
class PassphraseSaver:
@ -33,11 +32,11 @@ class PassphraseSaver:
def _read(self, card):
# Return a list of saved passphrases, or empty list if fail.
# Fail silently in all cases. Expect to see lots of noise here.
decrypt = tcc.AES(tcc.AES.CTR | tcc.AES.Decrypt, self.key)
decrypt = ngu.aes.CTR(self.key)
try:
msg = open(self.filename(card), 'rb').read()
txt = decrypt.update(msg)
txt = decrypt.cipher(msg)
return ujson.loads(txt)
except:
return []
@ -46,7 +45,7 @@ class PassphraseSaver:
async def append(self, xfp, bip39pw):
# encrypt and save; always appends.
from ux import ux_dramatic_pause
from main import dis
from glob import dis
from actions import needs_microsd
while 1:
@ -60,9 +59,9 @@ class PassphraseSaver:
data.append(dict(xfp=xfp, pw=bip39pw))
encrypt = tcc.AES(tcc.AES.CTR | tcc.AES.Encrypt, self.key)
encrypt = ngu.aes.CTR(self.key)
msg = encrypt.update(ujson.dumps(data))
msg = encrypt.cipher(ujson.dumps(data))
with open(self.filename(card), 'wb') as fd:
fd.write(msg)
@ -130,7 +129,7 @@ class PassphraseSaver:
# apply the password immediately and drop them at top menu
set_bip39_passphrase(data[idx]['pw'])
from main import settings
from nvstore import settings
from utils import xfp2str
xfp = settings.get('xfp')

View File

@ -1,17 +1,16 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# See also: <https://github.com/micropython/micropython-lib/blob/master/LICENSE>
#
from ucollections import deque
from uasyncio.core import sleep
from uasyncio.core import sleep_ms
class QueueEmpty(Exception):
class QueueEmpty(BaseException):
"""Exception raised by get_nowait()."""
class QueueFull(Exception):
class QueueFull(BaseException):
"""Exception raised by put_nowait()."""
@ -26,7 +25,7 @@ class Queue:
with qsize(), since your single-threaded uasyncio application won't be
interrupted between calling qsize() and doing an operation on the Queue.
"""
_attempt_delay = 0.01
_attempt_delay = 10 # milliseconds
def __init__(self, maxsize=0):
self.maxsize = maxsize
@ -44,7 +43,7 @@ class Queue:
item = yield from queue.get()
"""
while not self._queue:
yield from sleep(self._attempt_delay)
yield from sleep_ms(self._attempt_delay)
return self._get()
def get_nowait(self):
@ -67,7 +66,7 @@ class Queue:
yield from queue.put(item)
"""
while self.qsize() >= self.maxsize and self.maxsize:
yield from sleep(self._attempt_delay)
yield from sleep_ms(self._attempt_delay)
self._put(val)
def put_nowait(self, val):

View File

@ -1,17 +1,25 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# random.py - subset of random module, with no compat, and using crypto-quality rng
#
from ckcc import rng
import ngu
# use this instead of rand%n
randbelow = ngu.random.uniform
# for bytes
bytes = ngu.random.bytes
# In-place list shuffle using Fisher-Yates algo
#
# see <https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Implementation_errors>
#
def shuffle(seq):
for i in range(len(seq)-1, 0, -1):
j = rng() % (i+1)
seq[i], seq[j] = seq[j], seq[i]
def shuffle(lst):
# cpython random.py:L286 -- Fisher-Yates
for i in reversed(range(1, len(lst))):
j = randbelow(i+1)
lst[i], lst[j] = lst[j], lst[i]
# EOF

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# seed.py - bip39 seeds and words
#
@ -13,40 +12,23 @@
#
from menu import MenuItem, MenuSystem
from utils import pop_count, xfp2str
import tcc, uctypes
import ngu, uctypes, bip39, random
from uhashlib import sha256
from ux import ux_show_story, the_ux, ux_dramatic_pause, ux_confirm
from ux import PressRelease
from pincodes import AE_SECRET_LEN, AE_LONG_SECRET_LEN
from actions import goto_top_menu
from stash import SecretStash, SensitiveValues
from ckcc import rng_bytes
from random import rng, shuffle
from ubinascii import hexlify as b2a_hex
from pwsave import PassphraseSaver
from nvstore import settings
from pincodes import pa
# seed words lengths we support: 24=>256 bits, and recommended
VALID_LENGTHS = (24, 18, 12)
# bit flag that means "also include bare prefix as a valid word"
_PREFIX_MARKER = const(1<<26)
def extend_word(w):
# try to add as many non-abiguous chars onto end,
# and append - if we had to stop before we got to final end
while 1:
bitmask = tcc.bip39.complete_word(w)
if bitmask == 0 or bitmask == _PREFIX_MARKER:
return w
if pop_count(bitmask) != 1:
return w+'-'
for n in range(26):
msk = 1<<n
if (msk & bitmask):
w += chr(97+n)
break
def letter_choices(sofar='', depth=0, thres=5):
# make a list of word completions based on indicated prefix
@ -56,33 +38,49 @@ def letter_choices(sofar='', depth=0, thres=5):
# - and q- which is really qu-, because English.
return [('%s-' % chr(97+i)) if i != 16 else 'qu-' for i in range(26) if i != 23]
bitmask = tcc.bip39.complete_word(sofar)
exact, nexts, matched = bip39.next_char(sofar)
#print("[%d] %s => x=%r n=%r m=%r" % (depth, sofar, exact, nexts, matched))
if not bitmask:
if not nexts:
# no more choices; done
return [sofar]
return [matched]
rv = []
if bitmask & _PREFIX_MARKER:
if exact:
# ie: "act" plus "action", "actor"
rv.append(sofar)
for w in (sofar+chr(i+97) for i in range(0, 26) if (bitmask & (1<<i))):
rv.append(extend_word(w))
if len(nexts) == 1 and matched:
# aba => abandon (unambig first 3 chars)
# but not: age => age, agent (abig first 3)
rv.append(matched)
else:
for w in nexts:
rv.append(sofar + w + '-')
if len(rv) <= thres and depth == 0:
# examples:
# z => ze- and zo- ... better if all 4 z-words are shown
# y => 6 choices
# - above thres=5, we get menus w/60+ entries
# - recurse only one level also to keep size down
a = []
for i in rv:
if i[-1] != '-':
a.append(i)
else:
a.extend(letter_choices(i[:-1], depth+1))
return a
# replace bab- => baby and other cases where prefix is unique
# - doesn't grow menu length
if len(sofar) >= 2:
for n, w in enumerate(rv):
if w[-1] != '-': continue
exact, nexts, matched = bip39.next_char(w[:-1])
if matched:
rv[n] = matched
if len(rv) <= thres:
if depth == 0:
# examples:
# z => ze- and zo- ... better if all 4 z-words are shown
# y => 6 choices
# - above thres=5, we get menus w/60+ entries
# - recurse only one level also to keep size down
a = []
for i in rv:
if i[-1] != '-':
a.append(i)
else:
a.extend(letter_choices(i[:-1], depth+1))
return a
return rv
@ -161,10 +159,27 @@ class WordNestMenu(MenuSystem):
#print(("words[%d]: " % len(words)) + ' '.join(words))
assert len(words) <= self.target_words
if len(words) == 23 and self.has_checksum:
# we can provide final 8 choices, but only for 24-word case
final_words = list(bip39.a2b_words_guess(words))
async def picks_chk_word(s, idx, choice):
# they picked final word, the word includes valid checksum bits
words.append(choice.label)
await cls.done_cb(words.copy())
items = [MenuItem(w, f=picks_chk_word) for w in final_words]
items.append(MenuItem('(none above)', f=self.explain_error))
return cls(is_commit=True, items=items)
# add a few top-items in certain cases
if len(words) == self.target_words:
if self.has_checksum:
correct = tcc.bip39.check(' '.join(words))
try:
bip39.a2b_words(' '.join(words))
correct = True
except ValueError:
correct = False
else:
correct = True
@ -259,10 +274,10 @@ async def show_words(words, prompt=None, escape=None, extra=''):
return await ux_show_story(msg, escape=escape, sensitive=True)
async def add_dice_rolls(count, seed, judge_them):
from main import dis
from glob import dis
from display import FontTiny, FontLarge
md = tcc.sha256(seed)
md = sha256(seed)
pr = PressRelease()
# fixed parts of screen
@ -331,18 +346,17 @@ async def import_from_dice():
await approve_word_list(seed)
async def make_new_wallet():
# Pick a new random seed, and
# Pick a new random seed.
await ux_dramatic_pause('Generating...', 4)
# always full 24-word (256 bit) entropy
seed = bytearray(32)
rng_bytes(seed)
seed = random.bytes(32)
assert len(set(seed)) > 4 # TRNG failure
# hash to mitigate bias in TRNG
seed = tcc.sha256(seed).digest()
# hash to mitigate possible bias in TRNG
seed = ngu.hash.sha256s(seed)
await approve_word_list(seed)
@ -353,7 +367,7 @@ async def approve_word_list(seed):
# vividly instructed, then it's a big deal to lose those words and have to start
# over. So confirm that action, and don't volunteer it.
words = tcc.bip39.from_data(seed).split(' ')
words = bip39.b2a_words(seed).split(' ')
assert len(words) == 24
while 1:
@ -373,7 +387,7 @@ async def approve_word_list(seed):
count, new_seed = await add_dice_rolls(0, seed, False)
if count:
seed = new_seed
words = tcc.bip39.from_data(seed).split(' ')
words = bip39.b2a_words(seed).split(' ')
continue
@ -405,11 +419,11 @@ async def approve_word_list(seed):
def set_seed_value(words):
# Save the seed words into secure element, and reboot. BIP39 password
# is not set at this point (empty string)
ok = tcc.bip39.check(' '.join(words))
assert ok, "seed check: %r" % words
bip39.a2b_words(words) # checksum check
# map words to bip39 wordlist indices
data = [tcc.bip39.lookup_word(w) for w in words]
data = [bip39.wordlist_en.index(w) for w in words]
# map to packed binary representation.
val = 0
@ -425,7 +439,7 @@ def set_seed_value(words):
seed = val.to_bytes(vlen, 'big')
assert len(seed) == vlen
from main import dis, pa, settings
from glob import dis
# encode it for our limited secret space
nv = SecretStash.encode(seed_phrase=seed)
@ -445,7 +459,7 @@ def set_bip39_passphrase(pw):
# apply bip39 passphrase for now (volatile)
# takes a bit, so show something
from main import dis
from glob import dis
dis.fullscreen("Working...")
# set passphrase
@ -466,7 +480,7 @@ def set_bip39_passphrase(pw):
async def remember_bip39_passphrase():
# Compute current xprv and switch to using that as root secret.
import stash
from main import dis, pa
from glob import dis
dis.fullscreen('Check...')
@ -488,7 +502,7 @@ async def remember_bip39_passphrase():
pa.login()
def clear_seed():
from main import dis, pa, settings
from glob import dis
import utime, version
dis.fullscreen('Clearing...')
@ -523,14 +537,14 @@ async def word_quiz(words, limited=None):
# truncate to some N randomly-selected words in the list
# and always the last word
order = list(range(wl-1))
shuffle(order)
random.shuffle(order)
order = order[0:limited-1]
order.append(wl-1)
else:
order = list(range(wl))
shuffle(order)
random.shuffle(order)
for o in order:
# always 3 choices: right answer, wrong from correct set, random word
@ -538,19 +552,19 @@ async def word_quiz(words, limited=None):
choices = [right]
while 1:
n = words[rng() % wl]
n = words[random.randbelow(wl)]
if n in choices: continue
choices.append(n)
break
while 1:
n = tcc.bip39.lookup_nth(rng() % 0x800)
n = bip39.wordlist_en[random.randbelow(0x800)]
if n in choices: continue
choices.append(n)
break
while 1:
shuffle(choices)
random.shuffle(choices)
msg = '\n'.join(' %d: %s' % (i+1, choices[i]) for i in range(3))
msg += '\n\nWhich word is right?\n\nX to give up, OK to see all the words again.'
@ -614,7 +628,7 @@ class PassphraseMenu(MenuSystem):
async def add_numbers(self, *a):
# collect a series of digits
from main import dis
from glob import dis
from display import FontTiny, FontSmall
global pp_sofar
@ -719,7 +733,6 @@ class PassphraseMenu(MenuSystem):
set_bip39_passphrase(pp_sofar)
from main import settings
xfp = settings.get('xfp')
msg = '''Above is the master key fingerprint of the new wallet.
@ -764,7 +777,7 @@ class SingleWordMenu(WordNestMenu):
async def spinner_edit(pw, confirm_exit=True):
# Allow them to pick each digit using "D-pad"
from main import dis
from glob import dis
from display import FontTiny, FontSmall
# Should allow full unicode, NKDN

View File

@ -1,17 +1,17 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# selftest.py - Interactive Selftest code
#
import ckcc
from uasyncio import sleep_ms
from main import dis, settings
from glob import dis
from display import FontLarge
from ux import ux_wait_keyup, ux_clear_keys, ux_poll_once
from ux import ux_show_story
from callgate import get_dfu_button, get_is_bricked, get_genuine, clear_genuine
from utils import imported
import version
from nvstore import settings
async def test_numpad():
# do an interactive self test
@ -32,7 +32,7 @@ async def test_numpad():
def set_genuine():
# PIN must be blank for this to work
# - or logged in already as main
from main import pa
from pincodes import pa
if pa.is_secondary:
return
@ -120,12 +120,12 @@ async def test_sflash():
dis.text(None, 18, 'Serial Flash')
dis.show()
from main import sf
from sflash import SF
from ustruct import pack
import tcc
import ngu
msize = 1024*1024
sf.chip_erase()
SF.chip_erase()
for phase in [0, 1]:
steps = 7*4
@ -133,9 +133,9 @@ async def test_sflash():
dis.progress_bar(i/steps)
dis.show()
await sleep_ms(250)
if not sf.is_busy(): break
if not SF.is_busy(): break
assert not sf.is_busy() # "didn't finish"
assert not SF.is_busy() # "didn't finish"
# leave chip blank
if phase == 1: break
@ -143,20 +143,20 @@ async def test_sflash():
buf = bytearray(32)
for addr in range(0, msize, 1024):
sf.read(addr, buf)
SF.read(addr, buf)
assert set(buf) == {255} # "not blank"
rnd = tcc.sha256(pack('I', addr)).digest()
sf.write(addr, rnd)
sf.read(addr, buf)
rnd = ngu.hash.sha256s(pack('I', addr))
SF.write(addr, rnd)
SF.read(addr, buf)
assert buf == rnd # "write failed"
dis.progress_bar_show(addr/msize)
# check no aliasing, also right size part
for addr in range(0, msize, 1024):
expect = tcc.sha256(pack('I', addr)).digest()
sf.read(addr, buf)
expect = ngu.hash.sha256s(pack('I', addr))
SF.read(addr, buf)
assert buf == expect # "readback failed"
dis.progress_bar_show(addr/msize)

View File

@ -1,4 +1,4 @@
# Additions Copyright 2018 by Coinkite Inc.
# Additions Copyright 2018-2021 by Coinkite Inc.
# Copyright (c) 2010 ArtForz -- public domain half-a-node
# Copyright (c) 2012 Jeff Garzik
# Copyright (c) 2010-2016 The Bitcoin Core developers
@ -15,25 +15,17 @@ CTransaction,CTxIn, CTxOut, etc....:
ser_*, deser_*: functions that handle serialization/deserialization
"""
from uio import BytesIO
from ubinascii import hexlify as b2a_hex
from ubinascii import unhexlify as a2b_hex
from ucollections import OrderedDict
import ustruct as struct
import tcc
import ngu
from opcodes import *
def sha256(s):
return tcc.sha256(s).digest()
def ripemd160(s):
return tcc.ripemd160(s).digest()
def hash256(s):
return sha256(sha256(s))
def hash160(s):
return ripemd160(sha256(s))
# single-shot hash functions
sha256 = ngu.hash.sha256s
ripemd160 = ngu.hash.ripemd160
hash256 = ngu.hash.sha256d
hash160 = ngu.hash.hash160
def bytes_to_hex_str(s):
return str(b2a_hex(s), 'ascii')
@ -239,15 +231,6 @@ def disassemble(script):
raise ValueError("bad script")
# Deserialize from a hex string representation (eg from RPC)
def FromHex(obj, hex_string):
obj.deserialize(BytesIO(hex_str_to_bytes(hex_string)))
return obj
# Convert a binary-serializable object to hex (eg for submission via RPC)
def ToHex(obj):
return bytes_to_hex_str(obj.serialize())
def ser_sig_der(r, s, sighash_type=1):
sig = b"\x30"

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# sffile.py - file-like objects stored in SPI Flash
#
@ -10,9 +9,10 @@
# - the offset is the file name
# - last 64k of memory reserved for settings
#
import tcc
from uasyncio import sleep_ms
from uio import BytesIO
from uhashlib import sha256
from sflash import SF
# this code works on large "blocks" defined by the chip as 64k
blksize = const(65536)
@ -33,13 +33,10 @@ class SFFile:
if max_size != None:
self.max_size = PADOUT(max_size) if not pre_erased else max_size
self.readonly = False
self.checksum = tcc.sha256()
self.checksum = sha256()
else:
self.readonly = True
from main import sf
self.sf = sf
def tell(self):
# where are we?
return self.pos
@ -74,32 +71,32 @@ class SFFile:
assert self.length == 0 # 'already wrote?'
for i in range(0, self.max_size, blksize):
self.sf.block_erase(self.start + i)
SF.block_erase(self.start + i)
if i and self.message:
from main import dis
from glob import dis
dis.progress_bar_show(i/self.max_size)
# expect block erase to take up to 2 seconds
while self.sf.is_busy():
while SF.is_busy():
await sleep_ms(50)
def __enter__(self):
if self.message:
from main import dis
from glob import dis
dis.fullscreen(self.message)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.message:
from main import dis
from glob import dis
dis.progress_bar_show(1)
return False
def wait_writable(self):
# TODO: timeouts here
while self.sf.is_busy():
while SF.is_busy():
pass
def write(self, b):
@ -128,7 +125,7 @@ class SFFile:
self.wait_writable()
self.sf.write(self.start + self.pos + sofar, here)
SF.write(self.start + self.pos + sofar, here)
left -= len(here)
sofar += len(here)
@ -155,12 +152,12 @@ class SFFile:
return b''
rv = bytearray(ll)
self.sf.read(self.start + self.pos, rv)
SF.read(self.start + self.pos, rv)
self.pos += ll
if self.message and ll > 1:
from main import dis
from glob import dis
dis.progress_bar_show(self.pos / self.length)
# altho tempting to return a bytearray (which we already have) many
@ -173,7 +170,7 @@ class SFFile:
if actual <= 0:
return 0
self.sf.read(self.start + self.pos, b)
SF.read(self.start + self.pos, b)
self.pos += actual

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# sflash.py - SPI Flash on rev D and up boards. Simple serial SPI flash on SPI2 port.
#
@ -116,7 +115,7 @@ class SPIFlash:
from nvstore import SLOTS
end = SLOTS[0]
from main import dis
from glob import dis
dis.fullscreen("Cleanup...")
for addr in range(0, end, self.BLOCK_SIZE):
@ -126,4 +125,7 @@ class SPIFlash:
while self.is_busy():
pass
# singleton
SF = SPIFlash()
# EOF

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# sram2.py - Jam some larger, long-lived objects into the SRAM2 area, which isn't used enough.
#

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# ssd1306.py - MicroPython SSD1306 OLED driver, I2C and SPI interfaces
#

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# stash.py - encoding the ultrasecrets: bip39 seeds and words
#
@ -11,8 +10,10 @@
# - 'abandon' * 17 + 'agent'
# - 'abandon' * 11 + 'about'
#
import tcc, uctypes, gc
import ngu, uctypes, gc, bip39
from uhashlib import sha256
from pincodes import AE_SECRET_LEN
from utils import swab32
def blank_object(item):
# Use/abuse uctypes to blank objects until python. Will likely
@ -24,7 +25,7 @@ def blank_object(item):
buf = uctypes.bytearray_at(addr, ln)
for i in range(ln):
buf[i] = 0
elif isinstance(item, tcc.bip32.HDNode):
elif isinstance(item, ngu.hdnode.HDNode):
item.blank()
else:
raise TypeError(item)
@ -58,28 +59,30 @@ class SecretStash:
elif xprv:
# master xprivkey, which could be a subkey of something we don't know
# - we record only the minimum
assert isinstance(xprv, tcc.bip32.HDNode)
assert isinstance(xprv, ngu.hdnode.HDNode)
nv[0] = 0x01
nv[1:33] = xprv.chain_code()
nv[33:65] = xprv.private_key()
nv[33:65] = xprv.privkey()
return nv
@staticmethod
def decode(secret, _bip39pw=''):
# expecting 72-bytes of secret payload; decode meaning
# expecting 72-bytes of secret payload; decode contents into objects
# returns:
# type, secrets bytes, HDNode(root)
#
marker = secret[0]
hd = ngu.hdnode.HDNode()
if marker == 0x01:
# xprv => BIP32 private key values
ch, pk = secret[1:33], secret[33:65]
assert not _bip39pw
return 'xprv', ch+pk, tcc.bip32.HDNode(chain_code=ch, private_key=pk,
child_num=0, depth=0, fingerprint=0)
hd.from_chaincode_privkey(ch, pk)
return 'xprv', ch+pk, hd
if marker & 0x80:
# seed phrase
@ -92,9 +95,9 @@ class SecretStash:
# make master secret, using the memonic words, and passphrase (or empty string)
seed_bits = secret[1:1+ll]
ms = tcc.bip39.seed(tcc.bip39.from_data(seed_bits), _bip39pw)
ms = bip39.master_secret(bip39.b2a_words(seed_bits), _bip39pw)
hd = tcc.bip32.from_seed(ms, 'secp256k1')
hd.from_master(ms)
return 'words', seed_bits, hd
@ -105,7 +108,7 @@ class SecretStash:
assert not _bip39pw
ms = secret[1:1+vlen]
hd = tcc.bip32.from_seed(ms, 'secp256k1')
hd = hd.from_master(ms)
return 'master', ms, hd
@ -118,7 +121,7 @@ class SensitiveValues:
def __init__(self, secret=None, bypass_pw=False):
if secret is None:
# fetch the secret from bootloader/atecc508a
from main import pa
from pincodes import pa
if pa.is_secret_blank():
raise ValueError('no secrets yet')
@ -176,11 +179,11 @@ class SensitiveValues:
def capture_xpub(self):
# track my xpubkey fingerprint & value in settings (not sensitive really)
# - we share these on any USB connection
from main import settings
from nvstore import settings
# Implicit in the values is the BIP39 encryption passphrase,
# which we might not want to actually store.
xfp = self.node.my_fingerprint()
xfp = swab32(self.node.my_fp())
xpub = self.chain.serialize_public(self.node)
if self._bip39pw:
@ -202,7 +205,7 @@ class SensitiveValues:
def derive_path(self, path, master=None, register=True):
# Given a string path, derive the related subkey
rv = (master or self.node).clone()
rv = (master or self.node).copy()
if register:
self.register(rv)
@ -212,15 +215,15 @@ class SensitiveValues:
if not i: continue # trailing or duplicated slashes
if i[-1] == "'":
assert len(i) >= 2, i
assert len(i) >= 2
is_hard = True
here = int(i[:-1])
assert 0 <= here < 0x80000000, here
here |= 0x80000000
else:
here = int(i)
assert 0 <= here < 0x80000000, here
is_hard = False
rv.derive(here)
assert 0 <= here < 0x80000000
rv.derive(here, is_hard)
return rv
@ -230,12 +233,12 @@ class SensitiveValues:
dirty = self.derive_path("m/2147431408'/0'/0'")
# clear the parent linkage by rebuilding it.
cc, pk = dirty.chain_code(), dirty.private_key()
cc, pk = dirty.chain_code(), dirty.privkey()
self.register(cc)
self.register(pk)
rv = tcc.bip32.HDNode(chain_code=cc, private_key=pk,
child_num=0, depth=0, fingerprint=0)
rv = ngu.hdnode.HDNode()
rv.from_chaincode_privkey(cc, pk)
self.register(rv)
return rv
@ -245,11 +248,11 @@ class SensitiveValues:
# 0x80000000 - 0xCC30 = 2147431376
node = self.derive_path("m/2147431408'/0'") # plan: 0' will be an index for other apps
acc = tcc.sha256(salt)
acc.update(node.private_key())
acc = sha256(salt)
acc.update(node.privkey())
acc.update(salt)
pk = tcc.sha256(acc.digest()).digest()
pk = ngu.hash.sha256s(acc.digest())
self.register(pk)
return pk

View File

@ -1,171 +0,0 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
#
# See also: <https://github.com/micropython/micropython-lib/blob/master/LICENSE>
#
import uerrno
import uselect as select
from uasyncio.core import *
class PollEventLoop(EventLoop):
def __init__(self, len=42):
EventLoop.__init__(self, len)
self.poller = select.poll()
self.objmap = {}
def add_reader(self, sock, cb, *args):
if args:
self.poller.register(sock, select.POLLIN)
self.objmap[id(sock)] = (cb, args)
else:
self.poller.register(sock, select.POLLIN)
self.objmap[id(sock)] = cb
def remove_reader(self, sock):
self.poller.unregister(sock)
del self.objmap[id(sock)]
def add_writer(self, sock, cb, *args):
if args:
self.poller.register(sock, select.POLLOUT)
self.objmap[id(sock)] = (cb, args)
else:
self.poller.register(sock, select.POLLOUT)
self.objmap[id(sock)] = cb
def remove_writer(self, sock):
try:
self.poller.unregister(sock)
self.objmap.pop(id(sock), None)
except OSError as e:
# StreamWriter.awrite() first tries to write to a socket,
# and if that succeeds, yield IOWrite may never be called
# for that socket, and it will never be added to poller. So,
# ignore such error.
if e.args[0] != uerrno.ENOENT:
raise
def wait(self, delay):
# We need one-shot behavior (second arg of 1 to .poll())
res = self.poller.ipoll(delay, 1)
# Remove "if res" workaround after
# https://github.com/micropython/micropython/issues/2716 fixed.
if res:
for sock, ev in res:
cb = self.objmap[id(sock)]
if ev & (select.POLLHUP | select.POLLERR):
# These events are returned even if not requested, and
# are sticky, i.e. will be returned again and again.
# If the caller doesn't do proper error handling and
# unregister this sock, we'll busy-loop on it, so we
# as well can unregister it now "just in case".
self.remove_reader(sock)
if isinstance(cb, tuple):
cb[0](*cb[1])
else:
cb.pend_throw(None)
self.call_soon(cb)
class StreamReader:
def __init__(self, polls, ios=None):
if ios is None:
ios = polls
self.polls = polls
self.ios = ios
def read(self, n=-1):
while True:
yield IORead(self.polls)
res = self.ios.read(n)
if res is not None:
break
# This should not happen for real sockets, but can easily
# happen for stream wrappers (ssl, websockets, etc.)
if not res:
yield IOReadDone(self.polls)
return res
def readexactly(self, n):
buf = b""
while n:
yield IORead(self.polls)
res = self.ios.read(n)
assert res is not None
if not res:
yield IOReadDone(self.polls)
break
buf += res
n -= len(res)
return buf
def readline(self):
buf = b""
while True:
yield IORead(self.polls)
res = self.ios.readline()
assert res is not None
if not res:
yield IOReadDone(self.polls)
break
buf += res
if buf[-1] == 0x0a:
break
return buf
def aclose(self):
yield IOReadDone(self.polls)
self.ios.close()
def __repr__(self):
return "<StreamReader %r %r>" % (self.polls, self.ios)
class StreamWriter:
def __init__(self, s, extra):
self.s = s
self.extra = extra
def awrite(self, buf, off=0, sz=-1):
# This method is called awrite (async write) to not proliferate
# incompatibility with original asyncio. Unlike original asyncio
# whose .write() method is both not a coroutine and guaranteed
# to return immediately (which means it has to buffer all the
# data), this method is a coroutine.
if sz == -1:
sz = len(buf) - off
while True:
res = self.s.write(buf, off, sz)
# If we spooled everything, return immediately
if res == sz:
return
if res is None:
res = 0
assert res < sz
off += res
sz -= res
yield IOWrite(self.s)
#assert s2.fileno() == self.s.fileno()
# Write piecewise content from iterable (usually, a generator)
def awriteiter(self, iterable):
for buf in iterable:
yield from self.awrite(buf)
def aclose(self):
yield IOWriteDone(self.s)
self.s.close()
def get_extra_info(self, name, default=None):
return self.extra.get(name, default)
def __repr__(self):
return "<StreamWriter %r>" % self.s
import uasyncio.core
uasyncio.core._event_loop_class = PollEventLoop

View File

@ -1,257 +0,0 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
#
# See also: <https://github.com/micropython/micropython-lib/blob/master/LICENSE>
#
import utime as time
import utimeq
type_gen = type((lambda: (yield))())
class TimeoutError(Exception):
pass
class EventLoop:
def __init__(self, len=42):
self.q = utimeq.utimeq(len)
# Current task being run. Task is a top-level coroutine scheduled
# in the event loop (sub-coroutines executed transparently by
# yield from/await, event loop "doesn't see" them).
self.cur_task = None
def time(self):
return time.ticks_ms()
def create_task(self, coro):
# CPython 3.4.2
self.call_later_ms(0, coro)
# CPython asyncio incompatibility: we don't return Task object
def call_soon(self, callback, *args):
self.call_at_(self.time(), callback, args)
def call_later(self, delay, callback, *args):
self.call_at_(time.ticks_add(self.time(), int(delay * 1000)), callback, args)
def call_later_ms(self, delay, callback, *args):
self.call_at_(time.ticks_add(self.time(), delay), callback, args)
def call_at_(self, time, callback, args=()):
self.q.push(time, callback, args)
def wait(self, delay):
# Default wait implementation, to be overriden in subclasses
# with IO scheduling
time.sleep_ms(delay)
def run_forever(self):
cur_task = [0, 0, 0]
while True:
if self.q:
# wait() may finish prematurely due to I/O completion,
# and schedule new, earlier than before tasks to run.
while 1:
t = self.q.peektime()
tnow = self.time()
delay = time.ticks_diff(t, tnow)
if delay < 0:
delay = 0
# Always call wait(), to give a chance to I/O scheduling
self.wait(delay)
if delay == 0:
break
self.q.pop(cur_task)
t = cur_task[0]
cb = cur_task[1]
args = cur_task[2]
self.cur_task = cb
# __main__.mem_info()
else:
self.wait(-1)
# Assuming IO completion scheduled some tasks
continue
if callable(cb):
cb(*args)
else:
delay = 0
try:
if args == ():
ret = next(cb)
else:
ret = cb.send(*args)
if isinstance(ret, SysCall1):
arg = ret.arg
if isinstance(ret, SleepMs):
delay = arg
elif isinstance(ret, IORead):
cb.pend_throw(False)
self.add_reader(arg, cb)
continue
elif isinstance(ret, IOWrite):
cb.pend_throw(False)
self.add_writer(arg, cb)
continue
elif isinstance(ret, IOReadDone):
self.remove_reader(arg)
elif isinstance(ret, IOWriteDone):
self.remove_writer(arg)
elif isinstance(ret, StopLoop):
return arg
else:
assert False, "Unknown syscall yielded: %r (of type %r)" % (ret, type(ret))
elif isinstance(ret, type_gen):
self.call_soon(ret)
elif isinstance(ret, int):
# Delay
delay = ret
elif ret is None:
# Just reschedule
pass
elif ret is False:
# Don't reschedule
continue
else:
assert False, "Unsupported coroutine yield value: %r (of type %r)" % (ret, type(ret))
except StopIteration as e:
continue
# Currently all syscalls don't return anything, so we don't
# need to feed anything to the next invocation of coroutine.
# If that changes, need to pass that value below.
self.call_later_ms(delay, cb)
def run_until_complete(self, coro):
def _run_and_stop():
yield from coro
yield StopLoop(0)
self.call_soon(_run_and_stop())
self.run_forever()
def stop(self):
self.call_soon((lambda: (yield StopLoop(0)))())
def close(self):
pass
class SysCall:
def __init__(self, *args):
self.args = args
def handle(self):
raise NotImplementedError
# Optimized syscall with 1 arg
class SysCall1(SysCall):
def __init__(self, arg):
self.arg = arg
class StopLoop(SysCall1):
pass
class IORead(SysCall1):
pass
class IOWrite(SysCall1):
pass
class IOReadDone(SysCall1):
pass
class IOWriteDone(SysCall1):
pass
_event_loop = None
_event_loop_class = EventLoop
def get_event_loop(len=42):
global _event_loop
if _event_loop is None:
_event_loop = _event_loop_class(len)
return _event_loop
def sleep(secs):
yield int(secs * 1000)
# Implementation of sleep_ms awaitable with zero heap memory usage
class SleepMs(SysCall1):
def __init__(self):
self.v = None
self.arg = None
def __call__(self, arg):
self.v = arg
#print("__call__")
return self
def __iter__(self):
#print("__iter__")
return self
def __next__(self):
if self.v is not None:
#print("__next__ syscall enter")
self.arg = self.v
self.v = None
return self
#print("__next__ syscall exit")
_stop_iter.__traceback__ = None
raise _stop_iter
_stop_iter = StopIteration()
sleep_ms = SleepMs()
class TimeoutObj:
def __init__(self, coro):
self.coro = coro
def wait_for_ms(coro, timeout):
def waiter(coro, timeout_obj):
res = yield from coro
timeout_obj.coro = None
return res
def timeout_func(timeout_obj):
if timeout_obj.coro:
prev = timeout_obj.coro.pend_throw(TimeoutError())
#print("prev pend", prev)
if prev is False:
_event_loop.call_soon(timeout_obj.coro)
timeout_obj = TimeoutObj(_event_loop.cur_task)
_event_loop.call_later_ms(timeout, timeout_func, timeout_obj)
return (yield from waiter(coro, timeout_obj))
def wait_for(coro, timeout):
return wait_for_ms(coro, int(timeout * 1000))
def coroutine(f):
return f
#
# The functions below are deprecated in uasyncio, and provided only
# for compatibility with CPython asyncio
#
def ensure_future(coro, loop=_event_loop):
_event_loop.call_soon(coro)
# CPython asyncio incompatibility: we don't return Task object
return coro
# CPython asyncio incompatibility: Task is a function, not a class (for efficiency)
def Task(coro, loop=_event_loop):
# Same as async()
_event_loop.call_soon(coro)

View File

@ -1,33 +0,0 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
#
# See also: <https://github.com/micropython/micropython-lib/blob/master/LICENSE>
#
from uasyncio import core
class Lock:
def __init__(self):
self.locked = False
self.wlist = []
def release(self):
assert self.locked
self.locked = False
if self.wlist:
#print(self.wlist)
coro = self.wlist.pop(0)
core.get_event_loop().call_soon(coro)
def acquire(self):
# As release() is not coro, assume we just released and going to acquire again
# so, yield first to let someone else to acquire it first
yield
#print("acquire:", self.locked)
while 1:
if not self.locked:
self.locked = True
return True
#print("putting", core.get_event_loop().cur_task, "on waiting list")
self.wlist.append(core.get_event_loop().cur_task)
yield False

View File

@ -1,19 +1,20 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# usb.py - USB related things
#
import ckcc, pyb, callgate, sys, ux, tcc, stash
from uasyncio import sleep_ms, IORead
import ckcc, pyb, callgate, sys, ux, ngu, stash, aes256ctr
from uasyncio import sleep_ms, core
from uhashlib import sha256
from public_constants import MAX_MSG_LEN, MAX_TXN_LEN, MAX_BLK_LEN, MAX_UPLOAD_LEN, AFC_SCRIPT
from public_constants import STXN_FLAGS_MASK
from ustruct import pack, unpack_from
from ubinascii import hexlify as b2a_hex
from ckcc import rng_bytes, watchpoint, is_simulator
from ckcc import watchpoint, is_simulator
import uselect as select
from utils import problem_file_line
from version import has_fatram
from utils import problem_file_line, call_later_ms
from version import has_fatram, is_devmode
from exceptions import FramingError, CCBusyError, HSMDenied
from nvstore import settings
# Unofficial, unpermissioned... numbers
COINKITE_VID = 0xd13e
@ -65,29 +66,21 @@ HSM_WHITELIST = frozenset({
# singleton instance of USBHandler()
handler = None
def enable_usb(loop, repl_enable=False):
# start it.
def enable_usb():
# We can't change it on the fly; must be disabled before here
cur = pyb.usb_mode()
# allow/block REPL access
ckcc.vcp_enabled(repl_enable)
if cur:
# We can't change it on the fly; must be disabled before here
print("USB already enabled")
print("USB already enabled: %s" % cur)
else:
# subclass, protocol, max packet length, polling interval, report descriptor
hid_info = (0x0, 0x0, 64, 5, hid_descp )
try:
pyb.usb_mode('VCP+HID', vid=COINKITE_VID, pid=CKCC_PID, hid=hid_info)
except:
assert False, 'bad usb mode'
return
pyb.usb_mode('VCP+HID', vid=COINKITE_VID, pid=CKCC_PID, hid=hid_info)
global handler
if loop and not handler:
if not handler:
handler = USBHandler()
loop.create_task(handler.usb_hid_recv())
from imptask import IMPT
IMPT.start_task('USB', handler.usb_hid_recv())
def is_vcp_active():
# VCP = Virtual Comm Port
@ -102,7 +95,7 @@ class USBHandler:
# We keep a running hash over whatever has been uploaded
# - reset at offset zero, can be read back anytime
self.file_checksum = tcc.sha256()
self.file_checksum = sha256()
# handle simulator
self.blockable = getattr(self.dev, 'pipe', self.dev)
@ -114,7 +107,7 @@ class USBHandler:
self.encrypted_req = False
# these will be tcc.AES objects later
# these will be objects later
self.encrypt = None
self.decrypt = None
@ -145,7 +138,7 @@ class USBHandler:
msg_len = 0
while 1:
yield IORead(self.blockable)
yield core._io_queue.queue_read(self.blockable)
try:
here, is_last, is_encrypted = self.get_packet()
@ -202,10 +195,15 @@ class USBHandler:
msg = 'Assertion ' + problem_file_line(exc)
resp = b'err_' + msg.encode()[0:80]
msg_len = 0
except MemoryError:
# prefer to catch at higher layers, but sometimes can't
resp = b'err_Out of RAM'
msg_len = 0
except Exception as exc:
# catch bugs and fuzzing too
print("USB request caused this: ", end='')
sys.print_exception(exc)
if is_simulator() or is_devmode:
print("USB request caused this: ", end='')
sys.print_exception(exc)
resp = b'err_Confused ' + problem_file_line(exc)
msg_len = 0
@ -214,26 +212,26 @@ class USBHandler:
except FramingError as exc:
reason = exc.args[0]
print("Framing: %s" % reason)
#print("Framing: %s" % reason)
self.framing_error(reason)
msg_len = 0
except BaseException as exc:
# recover from general issues/keep going
print("USB!")
sys.print_exception(exc)
#print("USB!")
#sys.print_exception(exc)
msg_len = 0
def decrypt_inplace(self, msg_len):
# self.msg is encrypted. decode it in place
# - seems dangerous to use memview here, but works
# - some memory alloc still happens here tho; probably in return of decrypt.update
self.msg[0:msg_len] = self.decrypt.update(memoryview(self.msg)[0:msg_len])
# - some memory alloc still happens here tho
self.msg[0:msg_len] = self.decrypt(memoryview(self.msg)[0:msg_len])
def encrypt_response(self, msg):
# encrypt what we'll send to desktop
return self.encrypt.update(msg)
return self.encrypt(msg)
async def send_response(self, resp):
# send a python object as the response
@ -251,7 +249,7 @@ class USBHandler:
elif isinstance(resp, int):
resp = pack('<4sI', 'int1', resp)
else:
print("Unknown resp: " + repr(resp))
#print("Unknown resp: " + repr(resp))
raise NotImplementedError()
assert len(resp) >= 4
@ -296,7 +294,7 @@ class USBHandler:
async def handle(self, cmd, args):
# Dispatch incoming message, and provide reply.
from main import hsm_active, is_devmode
from glob import hsm_active
try:
cmd = bytes(cmd).decode()
@ -309,7 +307,6 @@ class USBHandler:
from usb_test_commands import do_usb_command
return do_usb_command(cmd, args)
except:
raise
pass
if hsm_active:
@ -500,7 +497,6 @@ class USBHandler:
# bip39 passphrase provided, maybe use it if authorized
assert self.encrypted_req, 'must encrypt'
from auth import start_bip39_passphrase
from main import settings
assert settings.get('words', True), 'no seed'
assert len(args) < 400, 'too long'
@ -524,7 +520,7 @@ class USBHandler:
return self.handle_bag_number(args)
if has_fatram:
# HSM and user-related features only larger-memory Mk3
# HSM and user-related features only supported on larger-memory Mk3
if cmd == 'hsms':
# HSM mode "start" -- requires user approval
@ -584,22 +580,14 @@ class USBHandler:
# dryrun/testing purposes: validate only, doesn't unlock nothing
return b'asci' + Users.auth_okay(username, token, totp_time).encode('ascii')
print("USB garbage: %s +[%d]" % (cmd, len(args)))
#print("USB garbage: %s +[%d]" % (cmd, len(args)))
return b'err_Unknown cmd'
def call_after(self, func, *args):
from main import loop
async def doit():
# we want to provide nice response before dying
await sleep_ms(500)
func(*args)
loop.create_task(doit())
return None
# we want to provide nice response before dying
call_later_ms(500, func, *args)
def handle_crypto_setup(self, version, his_pubkey):
# pick a one-time key pair for myself, and return the pubkey for that
@ -608,24 +596,23 @@ class USBHandler:
assert len(his_pubkey) == 64
# pick a random key pair, just for this session
my_key = tcc.secp256k1.generate_secret()
my_pubkey = tcc.secp256k1.publickey(my_key, False)
pair = ngu.secp256k1.keypair()
my_pubkey = pair.pubkey().to_bytes(True) # un-compressed
#print('my pubkey = ' + str(b2a_hex(my_pubkey)))
#print('his pubkey = ' + str(b2a_hex(his_pubkey)))
pt = tcc.secp256k1.multiply(my_key, b'\x04' + his_pubkey)
#assert pt[0] == 4
self.session_key = tcc.sha256(pt[1:]).digest()
self.session_key = pair.ecdh_multiply(b'\x04' + his_pubkey)
del pair
#print("session = " + str(b2a_hex(self.session_key)))
# Would be nice to have nonce in addition to the counter, but
# library not ready for that, and also harder on the desktop side.
self.encrypt = tcc.AES(tcc.AES.CTR | tcc.AES.Encrypt, self.session_key)
self.decrypt = tcc.AES(tcc.AES.CTR | tcc.AES.Decrypt, self.session_key)
# harder on the desktop side.
ctr = aes256ctr.new(self.session_key)
self.encrypt = ctr.cipher
self.decrypt = ctr.copy().cipher
from main import settings
xfp = settings.get('xfp', 0)
xpub = settings.get('xpub', '')
@ -637,13 +624,13 @@ class USBHandler:
# - proves our identity and that no-one is between
# Rate limit and fuzz timing in case we have timing sensitivity
await sleep_ms(250 + tcc.random.uniform(1000))
await sleep_ms(100 + ngu.random.uniform(100))
with stash.SensitiveValues() as sv:
pk = sv.node.private_key()
pk = sv.node.privkey()
sv.register(pk)
signature = tcc.secp256k1.sign(pk, self.session_key)
signature = ngu.secp256k1.sign(pk, self.session_key).to_bytes()
assert len(signature) == 65
@ -653,7 +640,7 @@ class USBHandler:
async def handle_download(self, offset, length, file_number):
# let them read from where we store the signed txn
# - filenumber can be 0 or 1: uploaded txn, or result
from main import sf
from sflash import SF
# limiting memory use here, should be MAX_BLK_LEN really
length = min(length, MAX_BLK_LEN)
@ -664,26 +651,27 @@ class USBHandler:
# maintain a running SHA256 over what's sent
if offset == 0:
self.file_checksum = tcc.sha256()
self.file_checksum = sha256()
pos = (MAX_TXN_LEN * file_number) + offset
resp = bytearray(4 + length)
resp[0:4] = b'biny'
sf.read(pos, memoryview(resp)[4:])
SF.read(pos, memoryview(resp)[4:])
self.file_checksum.update(memoryview(resp)[4:])
return resp
async def handle_upload(self, offset, total_size, data):
from main import dis, sf, hsm_active
from sflash import SF
from glob import dis, hsm_active
from utils import check_firmware_hdr
from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE
# maintain a running SHA256 over what's received
if offset == 0:
self.file_checksum = tcc.sha256()
self.file_checksum = sha256()
assert offset % 256 == 0, 'alignment'
assert offset+len(data) <= total_size <= MAX_UPLOAD_LEN, 'long'
@ -699,10 +687,12 @@ class USBHandler:
# erase here
dis.fullscreen("Receiving...", offset/total_size)
sf.sector_erase(pos)
SF.sector_erase(pos)
while sf.is_busy():
await sleep_ms(10)
# expect 10-22 ms delay here
await sleep_ms(12)
while SF.is_busy():
await sleep_ms(2)
# write up to 256 bytes
here = data[pos-offset:pos-offset+256]
@ -721,10 +711,10 @@ class USBHandler:
if prob:
raise ValueError(prob)
sf.write(pos, here)
SF.write(pos, here)
# full page write: 0.6 to 3ms
while sf.is_busy():
while SF.is_busy():
await sleep_ms(1)
@ -746,7 +736,7 @@ class USBHandler:
subpath = cleanup_deriv_path(subpath)
from main import hsm_active
from glob import hsm_active
if hsm_active and not hsm_active.approve_xpub_share(subpath):
raise HSMDenied
@ -761,7 +751,8 @@ class USBHandler:
def handle_bag_number(self, bag_num):
import version, callgate
from main import dis, pa, is_devmode, settings
from glob import dis
from pincodes import pa
if version.is_factory_mode and bag_num:
# check state first

View File

@ -1,21 +1,22 @@
# (c) Copyright 2020 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# users.py
#
# Users, passwords and management of same. Primarily for HSM feature.
#
import ustruct, hmac, tcc
import ustruct, ngu
from public_constants import USER_AUTH_TOTP, USER_AUTH_HOTP, USER_AUTH_HMAC, USER_AUTH_SHOW_QR
from public_constants import MAX_USERNAME_LEN, PBKDF2_ITER_COUNT
from menu import MenuSystem, MenuItem
from ucollections import namedtuple
from ux import ux_dramatic_pause, ux_show_story, ux_confirm
from nvstore import settings
# accepting strings and strings, returning bytes when decoding, str when encoding (ie. correct)
b32encode = tcc.codecs.b32_encode
b32decode = tcc.codecs.b32_decode
b32encode = ngu.codecs.b32_encode
b32decode = ngu.codecs.b32_decode
hmac_sha256 = ngu.hmac.hmac_sha256
# to keep menus and such to a reasonable size
MAX_NUMBER_USERS = const(30)
@ -38,10 +39,10 @@ def calc_hotp(secret, counter):
msg = ustruct.pack('>Q', counter)
hmac_digest = hmac.new(secret, msg, tcc.sha1).digest()
md = ngu.hmac.hmac_sha1(secret, msg)
o = hmac_digest[19] & 15
token = ustruct.unpack('>I', hmac_digest[o:o + 4])[0] & 0x7fffffff
o = md[19] & 15
token = ustruct.unpack('>I', md[o:o + 4])[0] & 0x7fffffff
# return lowest 6 digits
return '%06d' % (token % 1000000)
@ -49,12 +50,13 @@ def calc_hotp(secret, counter):
def calc_hmac_key(text_password):
# Calculate a 32-byte key based on user's text password, PBKDF2_ITER_COUNT,
# and device serial number as salt.
# - before v4, this was pbkdf2_sha256
import version
salt = tcc.sha256(b'pepper' + version.serial_number().encode()).digest()
pw = tcc.pbkdf2('hmac-sha256', text_password, salt, PBKDF2_ITER_COUNT).key()
salt = ngu.hash.sha256s(b'pepper' + version.serial_number().encode())
pw = ngu.hash.pbkdf2_sha512(text_password, salt, PBKDF2_ITER_COUNT)
return pw
return pw[0:32]
def calc_local_pincode(psbt_sha, hmac_secret):
# Given a b64 encoded secret (shared from CC over USB) and the PSBT
@ -62,7 +64,7 @@ def calc_local_pincode(psbt_sha, hmac_secret):
from ubinascii import a2b_base64
key = a2b_base64(hmac_secret)
assert len(psbt_sha) == 32
digest = hmac.new(key, psbt_sha, tcc.sha256).digest()
digest = hmac_sha256(key, psbt_sha)
num = ustruct.unpack('>I', digest[-4:])[0] & 0x7fffffff
return '%06d' % (num % 1000000)
@ -79,7 +81,6 @@ class Users:
@classmethod
def get(cls):
from main import settings
rv = settings.get(KEY)
return rv or dict()
@ -91,7 +92,6 @@ class Users:
@classmethod
def update_counter(cls, username, cnt):
from main import settings
t = cls.get()
assert username in t
t[username][2] = cnt
@ -111,8 +111,6 @@ class Users:
# - username must be unique
# - if secret is empty, we pick it and return choice
# - show QR of secret (for TOTP/HOTP) if
from main import settings
qr_mode = bool(auth_mode & USER_AUTH_SHOW_QR)
if qr_mode:
auth_mode &= ~USER_AUTH_SHOW_QR
@ -168,8 +166,6 @@ class Users:
@classmethod
def delete(cls, username):
# remove a user. simple. no checking
from main import settings
u = cls.get()
u.pop(username, None)
settings.put(KEY, u)
@ -204,7 +200,7 @@ class Users:
secret = b32decode(secret)
if auth_mode == USER_AUTH_HMAC:
expect = hmac.new(secret, psbt_hash or bytes(32), tcc.sha256).digest()
expect = hmac_sha256(secret, psbt_hash or bytes(32))
if expect != token:
return 'mismatch'

View File

@ -1,12 +1,12 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# utils.py - Misc utils. My favourite kind of source file.
#
import gc, sys, ustruct, tcc
import gc, sys, ustruct, ngu
from ubinascii import unhexlify as a2b_hex
from ubinascii import hexlify as b2a_hex
from ubinascii import a2b_base64, b2a_base64
from uhashlib import sha256
B2A = lambda x: str(b2a_hex(x), 'ascii')
@ -85,14 +85,17 @@ def pop_count(i):
def get_filesize(fn):
# like os.path.getsize()
import uos
return uos.stat(fn)[6]
try:
return uos.stat(fn)[6]
except OSError:
return 0
class HexWriter:
# Emulate a file/stream but convert binary to hex as they write
def __init__(self, fd):
self.fd = fd
self.pos = 0
self.checksum = tcc.sha256()
self.checksum = sha256()
def __enter__(self):
self.fd.__enter__()
@ -376,12 +379,21 @@ def check_firmware_hdr(hdr, binary_size=None, bad_magic_ok=False):
def clean_shutdown(style=0):
# wipe SPI flash and shutdown (wiping main memory)
import callgate
from sflash import SF
try:
from main import sf
sf.wipe_most()
SF.wipe_most()
except: pass
callgate.show_logout(style)
def call_later_ms(delay, cb, *args):
import uasyncio
async def doit():
await uasyncio.sleep_ms(delay)
await cb(*args)
uasyncio.create_task(doit())
# EOF

View File

@ -1,11 +1,9 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# ux.py - UX/UI related helper functions
#
# NOTE: do not import from main at top.
from uasyncio import sleep_ms
from uasyncio.queues import QueueEmpty
from queues import QueueEmpty
import utime, gc
DEFAULT_IDLE_TIMEOUT = const(4*3600) # (seconds) 4 hours
@ -15,7 +13,7 @@ DEFAULT_IDLE_TIMEOUT = const(4*3600) # (seconds) 4 hours
# stack has already been updated, but the old
# top-of-stack code was waiting for a key event.
#
class AbortInteraction(Exception):
class AbortInteraction(BaseException):
pass
class UserInteraction:
@ -59,14 +57,14 @@ the_ux = UserInteraction()
def ux_clear_keys(no_aborts=False):
# flush any pending keypresses
from main import numpad
from glob import numpad
try:
while 1:
ch = numpad.get_nowait()
if not no_aborts and ch == numpad.ABORT_KEY:
raise AbortInteraction
raise AbortInteraction()
except QueueEmpty:
return
@ -74,14 +72,14 @@ def ux_clear_keys(no_aborts=False):
async def ux_wait_keyup(expected=None):
# Wait for single keypress in 'expected' set, return it
# no visual feedback, no escape
from main import numpad
from glob import numpad
armed = None
while 1:
ch = await numpad.get()
if ch == numpad.ABORT_KEY:
raise AbortInteraction
raise AbortInteraction()
if len(ch) > 1:
# multipress
@ -101,7 +99,7 @@ def ux_poll_once(expected='x'):
# - ignore and suppress any key not in expected
# - responds to key down only
# - eats any existing key presses
from main import numpad
from glob import numpad
while 1:
try:
@ -110,7 +108,7 @@ def ux_poll_once(expected='x'):
ch = numpad.get_nowait()
if ch == numpad.ABORT_KEY:
raise AbortInteraction
raise AbortInteraction()
except QueueEmpty:
return None
@ -126,7 +124,7 @@ class PressRelease:
self.num_repeats = 0
async def wait(self):
from main import numpad
from glob import numpad
armed = None
@ -150,7 +148,7 @@ class PressRelease:
ch = numpad.get_nowait()
if ch == numpad.ABORT_KEY:
raise AbortInteraction
raise AbortInteraction()
self.num_repeats = 0
@ -200,7 +198,7 @@ async def ux_show_story(msg, title=None, escape=None, sensitive=False, strict_es
# - returns character used to get out (X or Y)
# - can accept other chars to 'escape' as well.
# - accepts a stream or string
from main import dis, numpad
from glob import dis, numpad
from display import FontLarge
lines = []
@ -289,10 +287,10 @@ async def ux_show_story(msg, title=None, escape=None, sensitive=False, strict_es
async def idle_logout():
import main
from main import numpad, settings
import glob
from nvstore import settings
while not main.hsm_active:
while not glob.hsm_active:
await sleep_ms(250)
# they may have changed setting recently
@ -302,10 +300,10 @@ async def idle_logout():
now = utime.ticks_ms()
if not numpad.last_event_time:
if not glob.numpad.last_event_time:
continue
if now > numpad.last_event_time + timeout:
if now > glob.numpad.last_event_time + timeout:
# do a logout now.
print("Idle!")
@ -322,7 +320,7 @@ async def ux_confirm(msg):
async def ux_dramatic_pause(msg, seconds):
from main import dis, hsm_active
from glob import dis, hsm_active
if hsm_active:
return
@ -338,7 +336,7 @@ async def ux_dramatic_pause(msg, seconds):
def show_fatal_error(msg):
# show a multi-line error message, over some kinda "fatal" banner
from main import dis
from glob import dis
from display import FontTiny
dis.clear()
@ -375,13 +373,13 @@ def restore_menu():
def abort_and_goto(m):
# cancel any menu drill-down and show them some UX
from main import numpad
from glob import numpad
the_ux.reset(m)
numpad.abort_ux()
def abort_and_push(m):
# keep menu position, but interrupt it with a new UX
from main import numpad
from glob import numpad
the_ux.push(m)
numpad.abort_ux()
@ -427,7 +425,7 @@ class QRDisplay(UserInteraction):
def redraw(self):
# Redraw screen.
from main import dis
from glob import dis
from display import FontSmall, FontTiny
@ -509,7 +507,7 @@ async def ux_enter_number(prompt, max_value):
# return the decimal number which the user has entered
# - default/blank value assumed to be zero
# - clamps large values to the max
from main import dis
from glob import dis
from display import FontTiny
from math import log

View File

@ -1,5 +1,4 @@
# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# version.py - Get lots of different version numbers from stuff.
#
@ -39,7 +38,7 @@ def get_header_value(fld_name):
return ustruct.unpack_from(FWH_PY_FORMAT, hdr)[idx]
def is_devmode():
def get_is_devmode():
# what firmware signing key did we boot with? are we in dev mode?
from sigheader import RAM_HEADER_BASE, FWH_PK_NUM_OFFSET
import stm
@ -75,7 +74,7 @@ def serial_number():
def probe_system():
# run-once code to determine what hardware we are running on
global hw_label, has_608, has_fatram, is_factory_mode
global hw_label, has_608, has_fatram, is_factory_mode, is_devmode
from sigheader import RAM_BOOT_FLAGS, RBF_FACTORY_MODE
import ckcc, callgate, stm
@ -107,6 +106,9 @@ def probe_system():
# this path supports testing/dev with RDP!=2, which normal production bootroms enforce
is_factory_mode = False
# what firmware signing key did we boot with? are we in dev mode?
is_devmode = get_is_devmode()
probe_system()
# EOF

View File

@ -15,23 +15,34 @@ from ucollections import namedtuple
GlyphInfo = namedtuple('GlyphInfo', 'x y w h bits')
class FontBase:
pass
@classmethod
def lookup(cls, cp):
# lookup glyph data for a single codepoint, or return None
for r,d in cls._code_points:
if cp not in r: continue
ptr = d[cp-r.start]
if not ptr: return None
x,y, w,h, dlen = cls._bboxes[cls._bitmaps[ptr]]
bits = cls._bitmaps[ptr+1:ptr+1+dlen]
return GlyphInfo(x,y, w,h, bits)
return None
class FontSmall(FontBase):
height = 14
code_range = range(32, 215)
@staticmethod
def lookup(cp):
# lookup glyph data for a single codepoint, or return None
bboxes = [None, (0, -3, 7, 14, 0), (0, -3, 7, 14, 4), (0, -3,
_bboxes = [None, (0, -3, 7, 14, 0), (0, -3, 7, 14, 4), (0, -3,
7, 14, 5), (0, -3, 7, 14, 7), (0, -3, 7, 14, 9), (0, -3, 7, 14, 10),
(0, -3, 7, 14, 11), (0, -3, 7, 14, 12), (0, -3, 7, 14, 13), (0, -3,
7, 14, 14)]
code_points = [
_code_points = [
(range(32, 127), [1, 2, 14, 20, 31, 43, 55, 67, 73, 87, 101, 111, 122,
136, 144, 157, 170, 182, 194, 206, 218, 230, 242, 254, 266, 278,
290, 303, 317, 329, 339, 351, 363, 375, 387, 399, 411, 423, 435,
@ -44,7 +55,7 @@ class FontSmall(FontBase):
(range(215, 216), [1151]), # ×
]
bitmaps = b"""\
_bitmaps = b"""\
\xaa\x01\x07\x00\x10\x10\x10\x10\x10\x10\x10\x00\x10\x10\x03\x00\x28\x28\
\x28\x28\x06\x00\x00\x24\x24\x7e\x24\x24\x7e\x24\x24\x07\x00\x00\x10\x3c\
\x50\x50\x38\x14\x14\x78\x10\x07\x00\x22\x52\x54\x28\x08\x10\x14\x2a\x4a\
@ -111,32 +122,18 @@ class FontSmall(FontBase):
\x08\x08\x00\x7e\x08\x00\x00\x00\x00\x44\x44\x44\x44\x4c\x7a\x40\x40\x06\
\x00\x00\x00\x00\x42\x24\x18\x18\x24\x42\
"""
for r,d in code_points:
if cp not in r: continue
ptr = d[cp-r.start]
if not ptr: return None
x,y, w,h, dlen = bboxes[bitmaps[ptr]]
bits = bitmaps[ptr+1:ptr+1+dlen]
return GlyphInfo(x,y, w,h, bits)
return None
class FontLarge(FontBase):
height = 21
code_range = range(32, 215)
@staticmethod
def lookup(cp):
# lookup glyph data for a single codepoint, or return None
bboxes = [None, (0, -5, 10, 21, 0), (0, -5, 10, 21, 14), (0,
_bboxes = [None, (0, -5, 10, 21, 0), (0, -5, 10, 21, 14), (0,
-5, 10, 21, 16), (0, -5, 10, 21, 22), (0, -5, 10, 21, 26), (0, -5,
10, 21, 30), (0, -5, 10, 21, 32), (0, -5, 10, 21, 34), (0, -5, 10,
21, 36), (0, -5, 10, 21, 38), (0, -5, 10, 21, 40)]
code_points = [
_code_points = [
(range(32, 127), [1, 2, 35, 50, 81, 118, 151, 184, 199, 238, 277, 304,
335, 372, 395, 428, 463, 496, 529, 562, 595, 628, 661, 694, 727,
760, 793, 826, 863, 896, 923, 956, 989, 1022, 1055, 1088, 1121,
@ -150,7 +147,7 @@ class FontLarge(FontBase):
(range(215, 216), [3150]), # ×
]
bitmaps = b"""\
_bitmaps = b"""\
\xaa\x01\x07\x00\x00\x00\x00\x00\x00\x08\x00\x08\x00\x08\x00\x08\x00\x08\
\x00\x08\x00\x08\x00\x08\x00\x08\x00\x00\x00\x00\x00\x08\x00\x08\x00\x02\
\x00\x00\x00\x00\x00\x00\x11\x00\x11\x00\x11\x00\x11\x00\x06\x00\x00\x00\
@ -329,30 +326,15 @@ class FontLarge(FontBase):
\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x80\x21\
\x00\x12\x00\x0c\x00\x0c\x00\x12\x00\x21\x00\x40\x80\
"""
for r,d in code_points:
if cp not in r: continue
ptr = d[cp-r.start]
if not ptr: return None
x,y, w,h, dlen = bboxes[bitmaps[ptr]]
bits = bitmaps[ptr+1:ptr+1+dlen]
return GlyphInfo(x,y, w,h, bits)
return None
class FontTiny(FontBase):
height = 6
code_range = range(32, 8730)
@staticmethod
def lookup(cp):
# lookup glyph data for a single codepoint, or return None
bboxes = [None, (0, -1, 4, 6, 0), (0, -1, 4, 6, 2), (0, -1, 4,
_bboxes = [None, (0, -1, 4, 6, 0), (0, -1, 4, 6, 2), (0, -1, 4,
6, 3), (0, -1, 4, 6, 4), (0, -1, 4, 6, 5), (0, -1, 4, 6, 6)]
code_points = [
_code_points = [
(range(32, 127), [1, 2, 8, 11, 17, 24, 30, 36, 39, 46, 53, 59, 65, 72,
76, 82, 88, 94, 100, 106, 112, 118, 124, 130, 136, 142, 148, 154,
161, 167, 172, 178, 184, 190, 196, 202, 208, 214, 220, 226, 232,
@ -366,7 +348,7 @@ class FontTiny(FontBase):
(range(8730, 8731), [580]), # √
]
bitmaps = b"""\
_bitmaps = b"""\
\xaa\x01\x05\x40\x40\x40\x00\x40\x02\xa0\xa0\x05\xa0\xf0\xa0\xf0\xa0\x06\
\x40\xe0\xc0\x20\xe0\x40\x05\x80\x20\x40\x80\x20\x05\x40\xa0\x40\xa0\x50\
\x02\x40\x40\x06\x20\x40\x40\x40\x40\x20\x06\x80\x40\x40\x40\x40\x80\x05\
@ -401,15 +383,4 @@ class FontTiny(FontBase):
\x80\x02\x50\xa0\x05\x40\xe0\x40\x00\xe0\x06\x00\xa0\xa0\xa0\xc0\x80\x04\
\x00\xa0\x40\xa0\x05\x30\x20\x20\xa0\x60\
"""
for r,d in code_points:
if cp not in r: continue
ptr = d[cp-r.start]
if not ptr: return None
x,y, w,h, dlen = bboxes[bitmaps[ptr]]
bits = bitmaps[ptr+1:ptr+1+dlen]
return GlyphInfo(x,y, w,h, bits)
return None

7
stm32/.gitignore vendored
View File

@ -7,6 +7,7 @@ build/
mpy-files.timestamp
firmware.lss
firmware-signed.*
firmware.elf
# somewhat useful binary snapshots
mostly.dfu
@ -14,3 +15,9 @@ firmware-signed.dfu
dev.bin
dev.dfu
# byproducts of check-repro target
check-fw.bin
check-bootrom.bin
repro-got.txt
repro-want.txt

1
stm32/COLDCARD/c-modules Symbolic link
View File

@ -0,0 +1 @@
../../external/c-modules

View File

@ -1,6 +1,5 @@
/*
* (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
* and is covered by GPLv3 license found in COPYING.
* (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
*
* based on ports/stm32/mpconfigport.h
*
@ -91,8 +90,8 @@ extern bool CKCC_flash_bdev_writeblock(const uint8_t *src, uint32_t block);
#define MICROPY_USE_INTERNAL_ERRNO (1)
#define MICROPY_ENABLE_SCHEDULER (1)
#define MICROPY_SCHEDULER_DEPTH (8)
#define MICROPY_VFS (1)
#define MICROPY_VFS_FAT (1)
//?//#define MICROPY_VFS (1)
//?//#define MICROPY_VFS_FAT (1)
// control over Python builtins
#define MICROPY_PY_FUNCTION_ATTRS (1)
@ -356,6 +355,7 @@ static inline mp_uint_t disable_irq(void) {
// We need an implementation of the log2 function which is not a macro
#define MP_NEED_LOG2 (1)
#if 0
// There is no classical C heap in bare-metal ports, only Python
// garbage-collected heap. For completeness, emulate C heap via
// GC heap. Note that MicroPython core never uses malloc() and friends,
@ -363,6 +363,7 @@ static inline mp_uint_t disable_irq(void) {
#define malloc(n) m_malloc(n)
#define free(p) m_free(p)
#define realloc(p, n) m_realloc(p, n)
#endif
// see stm32f4XX_hal_conf.h USE_USB_FS & USE_USB_HS
// at the moment only USB_FS is supported
@ -372,12 +373,6 @@ static inline mp_uint_t disable_irq(void) {
// We need to provide a declaration/definition of alloca()
#include <alloca.h>
// not wanted
#undef MICROPY_HW_ENABLE_DHT
#define MICROPY_HW_ENABLE_DHT (0)
#undef MICROPY_PY_MACHINE_PULSE
#define MICROPY_PY_MACHINE_PULSE (0)
#define MICROPY_PY_COLLECTIONS_DEQUE (1)

View File

@ -1 +0,0 @@
../../external/modcryptocurrency/crc.c

View File

@ -1 +0,0 @@
../../external/crypto

View File

@ -3,12 +3,12 @@
//
// AUTO-generated.
//
// built: 2021-01-14
// version: 3.2.2
// built: 2021-03-17
// version: 4.0.0b4
//
#include <stdint.h>
// this overrides ports/stm32/fatfs_port.c
uint32_t get_fattime(void) {
return 0x522e1840UL;
return 0x52712000UL;
}

View File

@ -1,6 +1,5 @@
/*
* (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
* and is covered by GPLv3 license found in COPYING.
* (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
*
* Based on part of ports/stm32/main.c
*
@ -35,6 +34,7 @@
#include <string.h>
#include "py/runtime.h"
#include "py/mperrno.h"
#include "py/stackctrl.h"
#include "py/gc.h"
#include "py/mphal.h"
@ -49,157 +49,74 @@
// Important: system assumes this is always the /flash filesystem, mounted.
extern fs_user_mount_t fs_user_mount_flash;
// force_boot_py_contents()
//
static void
force_boot_py_contents(FATFS *fs)
{
static const char fresh_boot_py[] =
"# boot.py - CANNOT CHANGE\r\n"
"import machine, pyb, sys, os\r\n"
"from machine import bootloader as dfu\r\n"
"from machine import reset\r\n"
"\r\n"
"from main import loop, go; go()\r\n"
;
// Always replace boot.py with correct content. Never run modified
// version that might be there already.
FIL fp;
UINT n;
FRESULT res;
res = f_open(fs, &fp, "/boot.py", FA_READ);
if(res == FR_OK) {
// verify contents
uint8_t buf[sizeof(fresh_boot_py) + 10];
UINT actual = 0;
res = f_read(&fp, buf, sizeof(buf), &actual);
f_close(&fp);
if(res == FR_OK && actual == sizeof(fresh_boot_py)-1) {
// right size, but check contents too
if(memcmp(buf, fresh_boot_py, sizeof(fresh_boot_py) - 1) == 0) {
// good!
return;
}
}
}
// re-write it
f_open(fs, &fp, "/boot.py", FA_WRITE | FA_CREATE_ALWAYS);
f_write(&fp, fresh_boot_py, sizeof(fresh_boot_py) - 1, &n);
f_close(&fp);
printf("initfs: rewrote boot.py\n");
}
// Replace standard version of this function, so it's useful for this project.
//
MP_NOINLINE bool init_flash_fs(uint reset_mode)
{
int factory_reset_create_filesystem(void) {
fs_user_mount_t vfs;
pyb_flash_init_vfs(&vfs);
uint8_t working_buf[FF_MAX_SS];
FRESULT res = f_mkfs(&vfs.fatfs, FM_FAT, 0, working_buf, sizeof(working_buf));
if (res != FR_OK) {
mp_printf(&mp_plat_print, "MPY: can't create flash filesystem\n");
return -MP_ENODEV;
}
static const char fresh_readme_txt[] =
"Coldcard Wallet: Virtual Disk\r\n"
"\r\n"
"Developers can put files into /lib to use in place of normal Coldcard code.\r\n"
"\r\n"
"- reference data could also be stored, for your code to read\r\n"
"- stock firmware does not use this area for anything\r\n"
"- this area mounted at /flash during normal bootup\r\n"
"- limited to 128k, must be FAT32\r\n"
"- there is a menu command to reset this area to stock values\r\n"
"- contents will survive firmware upgrades\r\n"
;
// init the vfs object
fs_user_mount_t *vfs_fat = &fs_user_mount_flash;
vfs_fat->flags = 0;
pyb_flash_init_vfs(vfs_fat);
// set volume label, which becomes mountpoint on MacOS
f_setlabel(&vfs.fatfs, "COLDCARD");
// try to mount the flash
FRESULT res = f_mount(&vfs_fat->fatfs);
FIL fp;
UINT n;
if (reset_mode == 3 || res == FR_NO_FILESYSTEM) {
// no filesystem, or asked to reset it, so create a fresh one
// create readme file
f_open(&vfs.fatfs, &fp, "/README.txt", FA_WRITE | FA_CREATE_ALWAYS);
f_write(&fp, fresh_readme_txt, sizeof(fresh_readme_txt) - 1, &n);
f_close(&fp);
uint8_t working_buf[_MAX_SS];
res = f_mkfs(&vfs_fat->fatfs, FM_FAT, 0, working_buf, sizeof(working_buf));
if (res == FR_OK) {
// success creating fresh LFS
} else {
printf("PYB: can't create flash filesystem\n");
return false;
}
// make required subdirs
f_mkdir(&vfs.fatfs, "/lib");
// set volume label, which becomes mountpoint on MacOS
f_setlabel(&vfs_fat->fatfs, "COLDCARD");
// We don't need this anymore, but seems harmless to keep it.
f_open(&vfs.fatfs, &fp, "/SKIPSD", FA_WRITE | FA_CREATE_ALWAYS);
f_write(&fp, "y", 1, &n);
f_close(&fp);
FIL fp;
UINT n;
// create an ident file, or two
// - algo matches shared/version.py serial_number() function
{ char fname[80];
const uint8_t *id = (const uint8_t *)MP_HAL_UNIQUE_ID_ADDRESS; // 12 bytes, binary
snprintf(fname, sizeof(fname),
"ckcc-%02X%02X%02X%02X%02X%02X.txt",
id[11], id[10] + id[2], id[9], id[8] + id[0], id[7], id[6]);
// create readme file
f_open(&vfs_fat->fatfs, &fp, "/README.txt", FA_WRITE | FA_CREATE_ALWAYS);
f_write(&fp, fresh_readme_txt, sizeof(fresh_readme_txt) - 1, &n);
f_open(&vfs.fatfs, &fp, fname, FA_WRITE | FA_CREATE_ALWAYS);
f_write(&fp, fname+5, 12, &n);
f_close(&fp);
// make required subdirs
f_mkdir(&vfs_fat->fatfs, "/lib");
// XXX need this due to a line of code in stm32/main.c
f_open(&vfs_fat->fatfs, &fp, "/SKIPSD", FA_WRITE | FA_CREATE_ALWAYS);
f_write(&fp, "y", 1, &n);
f_open(&vfs.fatfs, &fp, "serial.txt", FA_WRITE | FA_CREATE_ALWAYS);
f_write(&fp, fname+5, 12, &n);
f_close(&fp);
// create an ident file, or two
// - algo matches shared/version.py serial_number() function
{ char fname[80];
const uint8_t *id = (const uint8_t *)MP_HAL_UNIQUE_ID_ADDRESS; // 12 bytes, binary
snprintf(fname, sizeof(fname),
"ckcc-%02X%02X%02X%02X%02X%02X.txt",
id[11], id[10] + id[2], id[9], id[8] + id[0], id[7], id[6]);
f_open(&vfs_fat->fatfs, &fp, fname, FA_WRITE | FA_CREATE_ALWAYS);
f_write(&fp, fname+5, 12, &n);
f_close(&fp);
f_open(&vfs_fat->fatfs, &fp, "serial.txt", FA_WRITE | FA_CREATE_ALWAYS);
f_write(&fp, fname+5, 12, &n);
f_close(&fp);
}
} else if (res == FR_OK) {
// mount sucessful
} else {
fail:
printf("PYB: can't mount flash\n");
return false;
}
// mount the flash device (there should be no other devices mounted at this point)
// we allocate this structure on the heap because vfs->next is a root pointer
mp_vfs_mount_t *vfs = m_new_obj_maybe(mp_vfs_mount_t);
if (vfs == NULL) {
goto fail;
}
vfs->str = "/flash";
vfs->len = 6;
vfs->obj = MP_OBJ_FROM_PTR(vfs_fat);
vfs->next = NULL;
MP_STATE_VM(vfs_mount_table) = vfs;
// The current directory is used as the boot up directory.
// It is set to the internal flash filesystem by default.
MP_STATE_PORT(vfs_cur) = vfs;
// Make sure we have a /flash/boot.py. Create it if needed, also verify and force contents
force_boot_py_contents(&vfs_fat->fatfs);
// LATER:
// - v3 and earlier relied on the contents of boot.py to operate correctly,
// so at bootup it verified contents and forced it.
// - in v4, file is ignored, so we could delete but let's perserve easy downgrade
// and just do nothing.
//force_boot_py_contents(&vfs.fatfs);
return true;
return 0; // success
}
// EOF

View File

@ -35,6 +35,7 @@ _minimum_heap_size = 16K;
above last byte of RAM. Note that EABI requires the stack to be 8-byte
aligned for a call. */
_estack = ORIGIN(SRAM2) + LENGTH(SRAM2);
_sstack = ORIGIN(SRAM2);
/* RAM extents for the garbage collector */
_ram_start = ORIGIN(RAM);

View File

@ -1,6 +1,5 @@
//
// (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
// and is covered by GPLv3 license found in COPYING.
// (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
//
// modckcc.c - module for Coldcard hardware features and glue.
//
@ -9,6 +8,7 @@
#include "modckcc.h"
#include "rng.h"
#include "usb.h"
#include "flash.h"
#include "bufhelper.h"
#include "py/gc.h"
@ -16,6 +16,7 @@
#include "py/mphal.h"
#include "py/mpstate.h"
#include "py/stackctrl.h"
#include "boardctrl.h"
#include "storage.h"
#include "usb.h"
@ -208,7 +209,7 @@ STATIC mp_obj_t stack_limit(mp_obj_t new_val)
// Small values will cause immediate crash due to stack-depth checking, so avoid.
if((limit < 1024) || (limit > 64*1024)) {
mp_raise_ValueError("out of range");
mp_raise_ValueError(NULL);
}
mp_stack_set_limit(limit);
@ -227,9 +228,9 @@ STATIC mp_obj_t wipe_fs(void)
// after, it will be remounted already
// see initfs.c
extern bool init_flash_fs(uint reset_mode);
extern int factory_reset_create_filesystem(void);
return MP_OBJ_NEW_SMALL_INT(init_flash_fs(3));
return MP_OBJ_NEW_SMALL_INT(factory_reset_create_filesystem());
}
MP_DEFINE_CONST_FUN_OBJ_0(wipe_fs_obj, wipe_fs);
@ -275,6 +276,8 @@ const mp_obj_module_t ckcc_module = {
.globals = (mp_obj_dict_t*)&ckcc_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_ckcc, ckcc_module, 1);
void ckcc_early_init(void)
{
// Add system-wide init code here.
@ -282,6 +285,20 @@ void ckcc_early_init(void)
// Disable ^C to interrupt code.
// cannot find where this might be set by other code to ^C.
mp_interrupt_char = -1;
// Do the equivilent of "py.usb_mode(None)" in boot.py
extern mp_uint_t pyb_usb_flags;
pyb_usb_flags |= PYB_USB_FLAG_USB_MODE_CALLED;
}
void ckcc_boardctrl_before_boot_py(boardctrl_state_t *state)
{
// do not run /boot.py even if it exists
state->run_boot_py = false;
}
void ckcc_boardctrl_after_boot_py(boardctrl_state_t *state)
{
// nothing to do, no way to report failures anyway
}
// ckcc_heap_start()
@ -330,3 +347,24 @@ bool CKCC_flash_bdev_writeblock(const uint8_t *src, uint32_t block)
}
#endif
// There is no classical C heap in bare-metal ports, only Python
// garbage-collected heap. For completeness, emulate C heap via
// GC heap. Note that MicroPython core never uses malloc() and friends,
// but I need these for the C-language extensions I'm using.
void *malloc(size_t size)
{
return m_malloc(size);
}
void free(void *ptr)
{
m_free(ptr);
}
void *realloc(void *ptr, size_t size)
{
return m_realloc(ptr, size);
}
// EOF

View File

@ -1,6 +1,5 @@
/*
* (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
* and is covered by GPLv3 license found in COPYING.
* (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
*/
#ifndef MICROPY_INCLUDED_STMHAL_MODCKCC_H
#define MICROPY_INCLUDED_STMHAL_MODCKCC_H

View File

@ -1 +0,0 @@
../../external/modcryptocurrency

View File

@ -1 +0,0 @@
../../external/modcryptocurrency/modtcc.c

View File

@ -1,6 +1,5 @@
/*
* (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
* and is covered by GPLv3 license found in COPYING.
* (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
*/
#define MICROPY_HW_BOARD_NAME "Coldcard"
@ -12,10 +11,12 @@
#define MICROPY_HW_HAS_FLASH (1)
#define MICROPY_HW_HAS_SDCARD (1)
#define MICROPY_HW_HAS_LCD (0)
#define MICROPY_HW_ENABLE_RTC (0)
#define MICROPY_HW_ENABLE_HW_I2C (0)
#define HSE_VALUE ((uint32_t)8000000) /*!< Value of the External oscillator in Hz */
// don't want, but lots of modules interdepend on this
#define MICROPY_HW_ENABLE_RTC (0)
//#define HSE_VALUE ((uint32_t)8000000) /*!< Value of the External oscillator in Hz */
// USB config
#define MICROPY_HW_ENABLE_USB (1)
@ -38,13 +39,14 @@
#define MICROPY_HW_UARTn_IS_HALF_DUPLEX(n) ((n) == 4)
// no I2C at all
/*
// no debug serial port, sadly
#define MICROPY_HW_UART_REPL PYB_UART_2
#define MICROPY_HW_UART_REPL_BAUD 115200
*/
// I2C busses
/* I2C busses 00 none
#define MICROPY_HW_I2C1_SCL (pin_B6)
#define MICROPY_HW_I2C1_SDA (pin_B7)
#define MICROPY_HW_I2C2_SCL (pin_B10)
@ -64,19 +66,6 @@
#define MICROPY_HW_SPI2_MISO (pin_C2)
#define MICROPY_HW_SPI2_MOSI (pin_C3)
/*
// USRSW is pulled low. Pressing the button makes the input go high.
#define MICROPY_HW_USRSW_PIN (pin_C13)
#define MICROPY_HW_USRSW_PULL (GPIO_NOPULL)
#define MICROPY_HW_USRSW_EXTI_MODE (GPIO_MODE_IT_FALLING)
#define MICROPY_HW_USRSW_PRESSED (0)
// LEDs
#define MICROPY_HW_LED1 (pin_A5) // Green LD2 LED on Nucleo
#define MICROPY_HW_LED_ON(pin) (mp_hal_pin_high(pin))
#define MICROPY_HW_LED_OFF(pin) (mp_hal_pin_low(pin))
*/
// SD card detect switch
// - open when card inserted, grounded when no card
#define MICROPY_HW_SDCARD_DETECT_PIN (pin_A9)
@ -90,9 +79,6 @@
extern void ckcc_early_init(void);
#define MICROPY_BOARD_EARLY_INIT ckcc_early_init
// Pull in lots of crypto stuff
#define MICROPY_PY_TREZORCRYPTO (1)
// Need CRC32 for 7z support.
#define MICROPY_PY_UBINASCII_CRC32 (1)
@ -113,3 +99,21 @@ extern void *ckcc_heap_end(void);
#define MICROPY_HEAP_START ckcc_heap_start()
#define MICROPY_HEAP_END ckcc_heap_end()
// Features/or not.
#define MICROPY_HW_ENABLE_DHT (0)
#define MICROPY_HW_ENABLE_ADC (0)
// override some boot-up stuff
#define MICROPY_BOARD_BEFORE_BOOT_PY ckcc_boardctrl_before_boot_py
#define MICROPY_BOARD_AFTER_BOOT_PY ckcc_boardctrl_after_boot_py
struct _boardctrl_state_t;
extern void ckcc_boardctrl_before_boot_py(struct _boardctrl_state_t *state);
extern void ckcc_boardctrl_after_boot_py(struct _boardctrl_state_t *state);
#define MICROPY_HW_SDCARD_MOUNT_AT_BOOT (0)
#define MICROPY_HW_ENABLE_SDCARD (1)
#define MICROPY_HW_ENABLE_CARD_IDENT (1)
// EOF

View File

@ -4,45 +4,20 @@ MCU_SERIES = l4
CMSIS_MCU = STM32L475xx
AF_FILE = boards/stm32l476_af.csv
LD_FILES = boards/$(BOARD)/layout.ld boards/common_ifs.ld
OPENOCD_CONFIG = boards/openocd_stm32l4.cfg
# see py/mpconfig.h which uses this var if set
INC += -DMP_CONFIGFILE=\"boards/$(BOARD)/ckcc-port.h\"
#INC += -DMP_CONFIGFILE=\"boards/$(BOARD)/ckcc-port.h\"
# need the CDC inf file to be built before this file
initfs.c: $(GEN_CDCINF_HEADER)
# crypto code
CFLAGS_MOD += -Iboards/$(BOARD)/modcryptocurrency
CFLAGS_MOD += -Iboards/$(BOARD)/crypto
SRC_MOD += $(addprefix boards/$(BOARD)/crypto/,\
bignum.c ecdsa.c curves.c secp256k1.c nist256p1.c \
rand.c hmac.c pbkdf2.c \
bip32.c bip39.c base58.c base32.c segwit_addr.c \
address.c script.c \
ripemd160.c sha2.c sha3.c hasher.c \
blake256.c blake2b.c blake2s.c \
aes/aescrypt.c aes/aeskey.c aes/aestab.c aes/aes_modes.c \
ed25519-donna/curve25519-donna-32bit.c \
ed25519-donna/curve25519-donna-helpers.c \
ed25519-donna/modm-donna-32bit.c \
ed25519-donna/ed25519-donna-basepoint-table.c \
ed25519-donna/ed25519-donna-32bit-tables.c \
ed25519-donna/ed25519-donna-impl-base.c \
ed25519-donna/ed25519.c \
ed25519-donna/curve25519-donna-scalarmult-base.c \
ed25519-donna/ed25519-keccak.c \
ed25519-donna/ed25519-sha3.c \
chacha20poly1305/chacha20poly1305.c \
chacha20poly1305/chacha_merged.c \
chacha20poly1305/poly1305-donna.c \
chacha20poly1305/rfc7539.c )
SRC_MOD += ../../external/mpy-qr/moduqr.c
# settings that apply only to crypto C-lang code
build-COLDCARD/boards/COLDCARD/crypto/%.o: CFLAGS_MOD += \
-DUSE_BIP39_CACHE=0 -DBIP32_CACHE_SIZE=0 -DUSE_BIP32_CACHE=0 -DBIP32_CACHE_MAXDEPTH=0 \
-DRAND_PLATFORM_INDEPENDENT=1 -DUSE_BIP39_GENERATE=0 -DUSE_BIP32_25519_CURVES=0
# NgU and uQR libraries
NGU_NEEDS_CIFRA = 1
USER_C_MODULES = boards/$(BOARD)/c-modules
# the bulk of the COLDCARD-specific code
FROZEN_MANIFEST += boards/$(BOARD)/shared/manifest.py boards/manifest.py
# This will relocate things up by 32k=0x8000
# see also ./layout.ld
@ -51,11 +26,9 @@ TEXT0_ADDR = 0x08008000
TEXT1_ADDR = 0x0800C000
# don't want any of these: soft_spi, soft_qspi, dht
DRIVERS_SRC_C -= \
drivers/bus/softspi.c \
drivers/bus/softqspi.c \
drivers/memory/spiflash.c \
drivers/dht/dht.c
#DRIVERS_SRC_C -= drivers/bus/softspi.c \
# drivers/bus/softqspi.c drivers/memory/spiflash.c \
# drivers/dht/dht.c
# Approximately all the source code files?
ALL_SRC = $(SRC_LIB) $(SRC_LIBM) $(EXTMOD_SRC_C) $(DRIVERS_SRC_C) \
@ -63,20 +36,18 @@ ALL_SRC = $(SRC_LIB) $(SRC_LIBM) $(EXTMOD_SRC_C) $(DRIVERS_SRC_C) \
ALL_SRC += $(addprefix ports/stm32/, $(SRC_C))
ALL_SRC += py/*.[ch]
FROZEN_MPY_DIR = boards/$(BOARD)/frozen-modules
# XXX: this barely works
# XXX: this barely works, ignore errors
tags:
echo $(ALL_SRC)
cd $(TOP)/../..; pwd; ctags -R -f .tags $(addprefix external/micropython/, $(ALL_SRC)) \
(cd $(TOP)/../..; pwd; ctags -R -f .tags $(addprefix external/micropython/, $(ALL_SRC)) \
external/micropython/lib/stm32lib/STM32L4xx_HAL_Driver/*/*.[ch] \
external/micropython/lib/stm32lib/CMSIS/STM32L4xx/Include/stm32l475xx.h \
external/micropython/lib/cmsis/inc/core_cm4.h \
external/micropython/ports/stm32/*.[ch] \
external/micropython/ports/stm32/*/*.[ch] \
external/micropython/ports/stm32/usbdev/class \
external/crypto
external/micropython/ports/stm32/usbdev/{core,class}/{src,inc}/*.[ch] \
external/libngu/ngu/*.[ch])
sed -i .tmp '/dynruntime./d' $(TOP)/../../.tags
checksum:
@ -92,6 +63,7 @@ checksum:
# we always want debug symbols, since they get stripped anyway
COPT += -g
# bugfix IIRC
build-COLDCARD/boards/COLDCARD/modckcc.o: COPT = -O0 -DNDEBUG
files:
@ -100,3 +72,5 @@ files:
# SRC_HAL: $(SRC_HAL)
@echo
# CFLAGS: $(CFLAGS)
@echo
# FROZEN_MANIFEST: $(FROZEN_MANIFEST)

View File

@ -1,6 +1,5 @@
/*
* (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
* and is covered by GPLv3 license found in COPYING.
* (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
*
* based on ../../rng.c but more paranoid
*

View File

@ -1,6 +1,5 @@
/*
* (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
* and is covered by GPLv3 license found in COPYING.
* (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
*/
#pragma once

View File

@ -1,373 +1,20 @@
/**
******************************************************************************
* @file stm32l4xx_hal_conf.h
* @author MCD Application Team
* @version V1.2.0
* @date 25-November-2015
* @brief HAL configuration template file.
* This file should be copied to the application folder and renamed
* to stm32l4xx_hal_conf.h.
******************************************************************************
* @attention
*
* <h2><center>&copy; COPYRIGHT(c) 2015 STMicroelectronics</center></h2>
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of STMicroelectronics nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
******************************************************************************
*/
/* This file is part of the MicroPython project, http://micropython.org/
* The MIT License (MIT)
* Copyright (c) 2019 Damien P. George
*/
#ifndef MICROPY_INCLUDED_STM32L4XX_HAL_CONF_H
#define MICROPY_INCLUDED_STM32L4XX_HAL_CONF_H
/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __STM32L4xx_HAL_CONF_H
#define __STM32L4xx_HAL_CONF_H
#include "boards/stm32l4xx_hal_conf_base.h"
#ifdef __cplusplus
extern "C" {
#endif
// Oscillator values in Hz
#define HSE_VALUE (8000000)
#define LSE_VALUE (32768)
#define EXTERNAL_SAI1_CLOCK_VALUE (48000)
#define EXTERNAL_SAI2_CLOCK_VALUE (48000)
#define USE_USB_FS
/* Exported types ------------------------------------------------------------*/
/* Exported constants --------------------------------------------------------*/
// Oscillator timeouts in ms
#define HSE_STARTUP_TIMEOUT (100)
#define LSE_STARTUP_TIMEOUT (5000)
/* ########################## Module Selection ############################## */
/**
* @brief This is the list of modules to be used in the HAL driver
*/
#define HAL_MODULE_ENABLED
#define HAL_ADC_MODULE_ENABLED
#define HAL_CAN_MODULE_ENABLED
/* #define HAL_COMP_MODULE_ENABLED */
#define HAL_CORTEX_MODULE_ENABLED
/* #define HAL_CRC_MODULE_ENABLED */
/* #define HAL_CRYP_MODULE_ENABLED */
#define HAL_DAC_MODULE_ENABLED
/* #define HAL_DFSDM_MODULE_ENABLED */
#define HAL_DMA_MODULE_ENABLED
#define HAL_FIREWALL_MODULE_ENABLED
#define HAL_FLASH_MODULE_ENABLED
/* #define HAL_HCD_MODULE_ENABLED */
/* #define HAL_NAND_MODULE_ENABLED */
/* #define HAL_NOR_MODULE_ENABLED */
/* #define HAL_SRAM_MODULE_ENABLED */
#define HAL_GPIO_MODULE_ENABLED
#define HAL_I2C_MODULE_ENABLED
/* #define HAL_IRDA_MODULE_ENABLED */
/* #define HAL_IWDG_MODULE_ENABLED */
/* #define HAL_LCD_MODULE_ENABLED */
/* #define HAL_LPTIM_MODULE_ENABLED */
/* #define HAL_OPAMP_MODULE_ENABLED */
#define HAL_PCD_MODULE_ENABLED
#define HAL_PWR_MODULE_ENABLED
/* #define HAL_QSPI_MODULE_ENABLED */
#define HAL_RCC_MODULE_ENABLED
#define HAL_RNG_MODULE_ENABLED
#define HAL_RTC_MODULE_ENABLED
/* #define HAL_SAI_MODULE_ENABLED */
#define HAL_SD_MODULE_ENABLED
/* #define HAL_SMARTCARD_MODULE_ENABLED */
/* #define HAL_SMBUS_MODULE_ENABLED */
#define HAL_SPI_MODULE_ENABLED
/* #define HAL_SWPMI_MODULE_ENABLED */
#define HAL_TIM_MODULE_ENABLED
#define HAL_TSC_MODULE_ENABLED
#define HAL_UART_MODULE_ENABLED
/* #define HAL_USART_MODULE_ENABLED */
/* #define HAL_WWDG_MODULE_ENABLED */
/* ########################## Oscillator Values adaptation ####################*/
/**
* @brief Adjust the value of External High Speed oscillator (HSE) used in your application.
* This value is used by the RCC HAL module to compute the system frequency
* (when HSE is used as system clock source, directly or through the PLL).
*/
#if !defined (HSE_VALUE)
#define HSE_VALUE ((uint32_t)8000000) /*!< Value of the External oscillator in Hz */
#endif /* HSE_VALUE */
#if !defined (HSE_STARTUP_TIMEOUT)
#define HSE_STARTUP_TIMEOUT ((uint32_t)100) /*!< Time out for HSE start up, in ms */
#endif /* HSE_STARTUP_TIMEOUT */
/**
* @brief Internal Multiple Speed oscillator (MSI) default value.
* This value is the default MSI range value after Reset.
*/
#if !defined (MSI_VALUE)
#define MSI_VALUE ((uint32_t)4000000) /*!< Value of the Internal oscillator in Hz*/
#endif /* MSI_VALUE */
/**
* @brief Internal High Speed oscillator (HSI) value.
* This value is used by the RCC HAL module to compute the system frequency
* (when HSI is used as system clock source, directly or through the PLL).
*/
#if !defined (HSI_VALUE)
#define HSI_VALUE ((uint32_t)16000000) /*!< Value of the Internal oscillator in Hz*/
#endif /* HSI_VALUE */
/**
* @brief Internal Low Speed oscillator (LSI) value.
*/
#if !defined (LSI_VALUE)
#define LSI_VALUE ((uint32_t)32000) /*!< LSI Typical Value in Hz*/
#endif /* LSI_VALUE */ /*!< Value of the Internal Low Speed oscillator in Hz
The real value may vary depending on the variations
in voltage and temperature. */
/**
* @brief External Low Speed oscillator (LSE) value.
* This value is used by the UART, RTC HAL module to compute the system frequency
*/
#if !defined (LSE_VALUE)
#define LSE_VALUE ((uint32_t)32768) /*!< Value of the External oscillator in Hz*/
#endif /* LSE_VALUE */
#if !defined (LSE_STARTUP_TIMEOUT)
#define LSE_STARTUP_TIMEOUT ((uint32_t)5000) /*!< Time out for LSE start up, in ms */
#endif /* HSE_STARTUP_TIMEOUT */
/**
* @brief External clock source for SAI1 peripheral
* This value is used by the RCC HAL module to compute the SAI1 & SAI2 clock source
* frequency.
*/
#if !defined (EXTERNAL_SAI1_CLOCK_VALUE)
#define EXTERNAL_SAI1_CLOCK_VALUE ((uint32_t)48000) /*!< Value of the SAI1 External clock source in Hz*/
#endif /* EXTERNAL_SAI1_CLOCK_VALUE */
/**
* @brief External clock source for SAI2 peripheral
* This value is used by the RCC HAL module to compute the SAI1 & SAI2 clock source
* frequency.
*/
#if !defined (EXTERNAL_SAI2_CLOCK_VALUE)
#define EXTERNAL_SAI2_CLOCK_VALUE ((uint32_t)48000) /*!< Value of the SAI2 External clock source in Hz*/
#endif /* EXTERNAL_SAI2_CLOCK_VALUE */
/* Tip: To avoid modifying this file each time you need to use different HSE,
=== you can define the HSE value in your toolchain compiler preprocessor. */
/* ########################### System Configuration ######################### */
/**
* @brief This is the HAL system configuration section
*/
#define VDD_VALUE ((uint32_t)3300) /*!< Value of VDD in mv */
#define TICK_INT_PRIORITY ((uint32_t)0x00) /*!< tick interrupt priority */
#define USE_RTOS 0
#define PREFETCH_ENABLE 1
#define INSTRUCTION_CACHE_ENABLE 1
#define DATA_CACHE_ENABLE 1
/* ########################## Assert Selection ############################## */
/**
* @brief Uncomment the line below to expanse the "assert_param" macro in the
* HAL drivers code
*/
/* #define USE_FULL_ASSERT 1 */
/* Includes ------------------------------------------------------------------*/
/**
* @brief Include module's header file
*/
#ifdef HAL_RCC_MODULE_ENABLED
#include "stm32l4xx_hal_rcc.h"
#endif /* HAL_RCC_MODULE_ENABLED */
#ifdef HAL_GPIO_MODULE_ENABLED
#include "stm32l4xx_hal_gpio.h"
#endif /* HAL_GPIO_MODULE_ENABLED */
#ifdef HAL_DMA_MODULE_ENABLED
#include "stm32l4xx_hal_dma.h"
#endif /* HAL_DMA_MODULE_ENABLED */
#ifdef HAL_DFSDM_MODULE_ENABLED
#include "stm32l4xx_hal_dfsdm.h"
#endif /* HAL_DFSDM_MODULE_ENABLED */
#ifdef HAL_CORTEX_MODULE_ENABLED
#include "stm32l4xx_hal_cortex.h"
#endif /* HAL_CORTEX_MODULE_ENABLED */
#ifdef HAL_ADC_MODULE_ENABLED
#include "stm32l4xx_hal_adc.h"
#endif /* HAL_ADC_MODULE_ENABLED */
#ifdef HAL_CAN_MODULE_ENABLED
#include "stm32l4xx_hal_can.h"
#endif /* HAL_CAN_MODULE_ENABLED */
#ifdef HAL_COMP_MODULE_ENABLED
#include "stm32l4xx_hal_comp.h"
#endif /* HAL_COMP_MODULE_ENABLED */
#ifdef HAL_CRC_MODULE_ENABLED
#include "stm32l4xx_hal_crc.h"
#endif /* HAL_CRC_MODULE_ENABLED */
#ifdef HAL_CRYP_MODULE_ENABLED
#include "stm32l4xx_hal_cryp.h"
#endif /* HAL_CRYP_MODULE_ENABLED */
#ifdef HAL_DAC_MODULE_ENABLED
#include "stm32l4xx_hal_dac.h"
#endif /* HAL_DAC_MODULE_ENABLED */
#ifdef HAL_FIREWALL_MODULE_ENABLED
#include "stm32l4xx_hal_firewall.h"
#endif /* HAL_FIREWALL_MODULE_ENABLED */
#ifdef HAL_FLASH_MODULE_ENABLED
#include "stm32l4xx_hal_flash.h"
#endif /* HAL_FLASH_MODULE_ENABLED */
#ifdef HAL_SRAM_MODULE_ENABLED
#include "stm32l4xx_hal_sram.h"
#endif /* HAL_SRAM_MODULE_ENABLED */
#ifdef HAL_NOR_MODULE_ENABLED
#include "stm32l4xx_hal_nor.h"
#endif /* HAL_NOR_MODULE_ENABLED */
#ifdef HAL_NAND_MODULE_ENABLED
#include "stm32l4xx_hal_nand.h"
#endif /* HAL_NAND_MODULE_ENABLED */
#ifdef HAL_I2C_MODULE_ENABLED
#include "stm32l4xx_hal_i2c.h"
#endif /* HAL_I2C_MODULE_ENABLED */
#ifdef HAL_IWDG_MODULE_ENABLED
#include "stm32l4xx_hal_iwdg.h"
#endif /* HAL_IWDG_MODULE_ENABLED */
#ifdef HAL_LCD_MODULE_ENABLED
#include "stm32l4xx_hal_lcd.h"
#endif /* HAL_LCD_MODULE_ENABLED */
#ifdef HAL_LPTIM_MODULE_ENABLED
#include "stm32l4xx_hal_lptim.h"
#endif /* HAL_LPTIM_MODULE_ENABLED */
#ifdef HAL_OPAMP_MODULE_ENABLED
#include "stm32l4xx_hal_opamp.h"
#endif /* HAL_OPAMP_MODULE_ENABLED */
#ifdef HAL_PWR_MODULE_ENABLED
#include "stm32l4xx_hal_pwr.h"
#endif /* HAL_PWR_MODULE_ENABLED */
#ifdef HAL_QSPI_MODULE_ENABLED
#include "stm32l4xx_hal_qspi.h"
#endif /* HAL_QSPI_MODULE_ENABLED */
#ifdef HAL_RNG_MODULE_ENABLED
#include "stm32l4xx_hal_rng.h"
#endif /* HAL_RNG_MODULE_ENABLED */
#ifdef HAL_RTC_MODULE_ENABLED
#include "stm32l4xx_hal_rtc.h"
#endif /* HAL_RTC_MODULE_ENABLED */
#ifdef HAL_SAI_MODULE_ENABLED
#include "stm32l4xx_hal_sai.h"
#endif /* HAL_SAI_MODULE_ENABLED */
#ifdef HAL_SD_MODULE_ENABLED
#include "stm32l4xx_hal_sd.h"
#endif /* HAL_SD_MODULE_ENABLED */
#ifdef HAL_SMBUS_MODULE_ENABLED
#include "stm32l4xx_hal_smbus.h"
#endif /* HAL_SMBUS_MODULE_ENABLED */
#ifdef HAL_SPI_MODULE_ENABLED
#include "stm32l4xx_hal_spi.h"
#endif /* HAL_SPI_MODULE_ENABLED */
#ifdef HAL_SWPMI_MODULE_ENABLED
#include "stm32l4xx_hal_swpmi.h"
#endif /* HAL_SWPMI_MODULE_ENABLED */
#ifdef HAL_TIM_MODULE_ENABLED
#include "stm32l4xx_hal_tim.h"
#endif /* HAL_TIM_MODULE_ENABLED */
#ifdef HAL_TSC_MODULE_ENABLED
#include "stm32l4xx_hal_tsc.h"
#endif /* HAL_TSC_MODULE_ENABLED */
#ifdef HAL_UART_MODULE_ENABLED
#include "stm32l4xx_hal_uart.h"
#endif /* HAL_UART_MODULE_ENABLED */
#ifdef HAL_USART_MODULE_ENABLED
#include "stm32l4xx_hal_usart.h"
#endif /* HAL_USART_MODULE_ENABLED */
#ifdef HAL_IRDA_MODULE_ENABLED
#include "stm32l4xx_hal_irda.h"
#endif /* HAL_IRDA_MODULE_ENABLED */
#ifdef HAL_SMARTCARD_MODULE_ENABLED
#include "stm32l4xx_hal_smartcard.h"
#endif /* HAL_SMARTCARD_MODULE_ENABLED */
#ifdef HAL_WWDG_MODULE_ENABLED
#include "stm32l4xx_hal_wwdg.h"
#endif /* HAL_WWDG_MODULE_ENABLED */
#ifdef HAL_PCD_MODULE_ENABLED
#include "stm32l4xx_hal_pcd.h"
#endif /* HAL_PCD_MODULE_ENABLED */
#ifdef HAL_HCD_MODULE_ENABLED
#include "stm32l4xx_hal_hcd.h"
#endif /* HAL_HCD_MODULE_ENABLED */
/* Exported macro ------------------------------------------------------------*/
#ifdef USE_FULL_ASSERT
/**
* @brief The assert_param macro is used for function's parameters check.
* @param expr: If expr is false, it calls assert_failed function
* which reports the name of the source file and the source
* line number of the call that failed.
* If expr is true, it returns no value.
* @retval None
*/
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0)
#endif /* USE_FULL_ASSERT */
#ifdef __cplusplus
}
#endif
#endif /* __STM32L4xx_HAL_CONF_H */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
#endif // MICROPY_INCLUDED_STM32L4XX_HAL_CONF_H

View File

@ -1,6 +1,5 @@
/*
* (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
* and is covered by GPLv3 license found in COPYING.
* (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
*/
#include <string.h>
@ -11,7 +10,8 @@
#include "usb.h"
#include "uart.h"
bool ckcc_vcp_enabled = false;
//XXX/bool ckcc_vcp_enabled = false;
bool ckcc_vcp_enabled = true;
// replacements for more permissive versions found in stm32/mphalport.c
// - we don't support any h/w UARTs

View File

@ -11,16 +11,22 @@ PYTHON_DO_DFU = $(MPY_TOP)/tools/pydfu.py
# aka ../cli/signit.py
SIGNIT = signit
MAKE_ARGS = BOARD=COLDCARD -j 4
MAKE_ARGS = BOARD=COLDCARD -j 4 EXCLUDE_NGU_TESTS=1
all: COLDCARD/file_time.c
cd $(PORT_TOP) && $(MAKE) $(MAKE_ARGS)
clean-mpy:
rm -rf build
clean: clean-mpy
clean:
cd $(PORT_TOP) && $(MAKE) $(MAKE_ARGS) clean
git clean -xf built
# These trigger the 'all' target when we haven't completed a successful build yet
l-port/build-COLDCARD/firmware.elf: all
l-port/build-COLDCARD/firmware0.bin: all
l-port/build-COLDCARD/firmware1.bin: all
firmware.elf: l-port/build-COLDCARD/firmware.elf
cp l-port/build-COLDCARD/firmware.elf .
# These values used to make .DFU files. Flash memory locations.
FIRMWARE_BASE = 0x08008000
@ -28,13 +34,13 @@ BOOTLOADER_BASE = 0x08000000
FILESYSTEM_BASE = 0x080e0000
# Our version for this release.
VERSION_STRING = 3.2.2
VERSION_STRING = 4.0.0b4
#
# Sign and merge various parts
#
firmware-signed.bin: l-port/build-COLDCARD/firmware?.bin
$(SIGNIT) sign $(VERSION_STRING)
firmware-signed.bin: l-port/build-COLDCARD/firmware0.bin l-port/build-COLDCARD/firmware1.bin
$(SIGNIT) sign $(VERSION_STRING) -o $@
firmware-signed.dfu: firmware-signed.bin Makefile
$(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):$< $@
@ -42,13 +48,17 @@ firmware-signed.dfu: firmware-signed.bin Makefile
dfu: firmware-signed.dfu
# Build a binary, signed w/ production key
# - always rebuild binary
# - always rebuild binary for this one
.PHONY: dev.dfu
dev.dfu: l-port/build-COLDCARD/firmware?.bin
cd $(PORT_TOP) && $(MAKE) $(MAKE_ARGS)
$(SIGNIT) sign $(VERSION_STRING) -o dev.bin
$(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):dev.bin dev.dfu
.PHONY: remake
remake:
rm -rf l-port/build-COLDCARD/firmware?.bin l-port/build-COLDCARD/frozen_mpy*
# This is fast for Coinkite devs, but no DFU support in the wild.
up: dev.dfu
$(PYTHON_DO_DFU) -u dev.dfu
@ -61,26 +71,43 @@ COLDCARD/file_time.c: Makefile make_filetime.py
./make_filetime.py COLDCARD/file_time.c $(VERSION_STRING)
# Make a factory release: using key #1
# - when executed in a repro w/o the required key, it defaults to key zero
# - and that's what happens inside the Docker build
production.bin: firmware-signed.bin Makefile
$(SIGNIT) sign $(VERSION_STRING) -r firmware-signed.bin -k 1 -o production.bin
$(SIGNIT) sign $(VERSION_STRING) -r firmware-signed.bin -k 1 -o $@
# This is release of the bootloader that will be built into the release firmware.
BOOTLOADER_VERSION = 2.0.1
.PHONY: release
release: code-committed
$(MAKE) clean
$(MAKE) repro
test -f built/production.bin
$(MAKE) release-products
$(MAKE) tag-source
# This target just combines latest version of production firmware with bootrom into a DFU
# file, stored in ../releases with appropriately dated file name.
.PHONY: release
release: NEW_VERSION = $(shell $(SIGNIT) version production.bin)
release: RELEASE_FNAME = ../releases/$(NEW_VERSION)-coldcard.dfu
release: production.bin
.PHONY: release-products
release-products: NEW_VERSION = $(shell $(SIGNIT) version built/production.bin)
release-products: RELEASE_FNAME = ../releases/$(NEW_VERSION)-coldcard.dfu
release-products: built/production.bin
test ! -f $(RELEASE_FNAME)
$(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):production.bin \
cp built/file_time.c COLDCARD/file_time.c
-git commit COLDCARD/file_time.c -m "For $(NEW_VERSION)"
$(SIGNIT) sign $(VERSION_STRING) -r built/production.bin -k 1 -o built/production.bin
$(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):built/production.bin \
-b $(BOOTLOADER_BASE):bootloader/releases/$(BOOTLOADER_VERSION)/bootloader.bin \
$(RELEASE_FNAME)
@echo
@echo 'Made release: ' $(RELEASE_FNAME)
@echo
built/production.bin:
@echo "To make production build, must run docker code"
@false
# Use DFU to install the latest production version you have on hand
dfu-latest:
$(PYTHON_DO_DFU) -u `ls -t1 ../releases/*.dfu | head -1`
@ -97,11 +124,10 @@ code-committed:
@echo '... yes'
.PHONY: sign-release
sign-release: PUBLIC_VERSION = $(shell $(SIGNIT) version production.bin)
sign-release:
(cd ../releases; shasum -a 256 *.dfu *.md | sort -rk 2 | \
gpg --clearsign -u A3A31BAD5A2A5B10 --digest-algo SHA256 --output signatures.txt --yes - )
git commit -m "Signed for release: "$(PUBLIC_VERSION) ../releases/signatures.txt
git commit -m "Signed for release." ../releases/signatures.txt
# Tag source code associate with built release version.
# - do "make release" before this step!
@ -109,7 +135,7 @@ sign-release:
# - this target will "git add" the new binary in ../release/*dfu
# - update & sign signatures file
# - and tag everything
tag-source: PUBLIC_VERSION = $(shell $(SIGNIT) version production.bin)
tag-source: PUBLIC_VERSION = $(shell $(SIGNIT) version built/production.bin)
tag-source: sign-release code-committed
git add ../releases/$(PUBLIC_VERSION)-coldcard.dfu
git commit -am "New release: "$(PUBLIC_VERSION)
@ -161,14 +187,9 @@ PY_FILES = $(shell find ../shared -name \*.py)
ALL_MPY_FILES = $(addprefix build/, $(PY_FILES:../shared/%.py=%.mpy))
MPY_FILES = $(filter-out build/obsolete/%, $(ALL_MPY_FILES))
build/%.mpy: ../shared/%.py Makefile
mkdir -p $(dir $@)
$(MPY_CROSS) -o $@ -s $*.py $<
# In another window:
#
# openocd -f l-port/boards/openocd_stm32l4.cfg
# openocd -f openocd_stm32l4x6.cfg
#
# Can do:
# - "load" which writes the flash (medium speed, lots of output on st-util)
@ -178,11 +199,11 @@ build/%.mpy: ../shared/%.py Makefile
# - and so on
#
debug:
arm-none-eabi-gdb $(PORT_TOP)/build-COLDCARD/firmware.elf -x gogo.gdb
arm-none-eabi-gdb built/firmware.elf -x gogo.gdb
# detailed listing, very handy
OBJDUMP = arm-none-eabi-objdump
firmware.lss: $(PORT_TOP)/build-COLDCARD/firmware.elf
firmware.lss: l-port/build-COLDCARD/firmware.elf
$(OBJDUMP) -h -S $< > $@
# Dump sizes of all frozen py files; requires recent build.
@ -200,9 +221,59 @@ size:
# one time setup, after repo checkout
setup:
cd $(MPY_TOP) ; git submodule update --init lib/stm32lib
cd $(MPY_TOP)/lib/stm32lib ; sed -i.orig -e 's/#define VECT_TAB_OFFSET 0x00/ /' \
CMSIS/STM32L4xx/Source/Templates/system_stm32l4xx.c
cd ../external/libngu; make min-one-time
cd $(MPY_TOP)/mpy-cross ; make
-ln -s $(PORT_TOP) l-port
-ln -s $(MPY_TOP) l-mpy
-cd $(PORT_TOP)/boards ; ln -s ../../../../../stm32/COLDCARD COLDCARD
cd $(PORT_TOP)/boards; if [ ! -L COLDCARD ]; then \
ln -s ../../../../../stm32/COLDCARD COLDCARD; fi
# Caution: docker container has read access to your source tree
# - a readonly copy of source tree, and one output directory
# - build products are copied to there, see repro-build.sh
# - works from this repo, but starts with copy of HEAD
DOCK_RUN_ARGS = -v $(realpath ..):/work/src:ro \
-v $(realpath built):/work/built:rw \
--privileged coldcard-build
repro: code-committed
docker build -t coldcard-build - < dockerfile.build
(cd ..; docker run $(DOCK_RUN_ARGS) sh src/stm32/repro-build.sh)
# debug: shell into docker container
shell:
docker run -it $(DOCK_RUN_ARGS) sh
# debug: allow docker to write into source tree
#DOCK_RUN_ARGS := -v $(realpath ..):/work/src:rw --privileged coldcard-build
PUBLISHED_BIN = $(wildcard ../releases/*-v$(VERSION_STRING)-coldcard.dfu)
# final step in repro-building: check you got the right bytes
# - but you don't have the production signing key, so that section is removed
check-repro: TRIM_SIG = sed -e 's/^00003f[89abcdef]0 .*/(firmware signature here)/'
check-repro: firmware-signed.bin
ifeq ($(PUBLISHED_BIN),)
@echo ""
@echo "No binary published yet for: $(VERSION_STRING)"
@echo ""
else
@echo Comparing against: $(PUBLISHED_BIN)
test -n "$(PUBLISHED_BIN)" -a -f $(PUBLISHED_BIN)
$(RM) -f check-fw.bin check-bootrom.bin
$(SIGNIT) split $(PUBLISHED_BIN) check-fw.bin check-bootrom.bin
$(SIGNIT) check check-fw.bin
$(SIGNIT) check firmware-signed.bin
hexdump -C firmware-signed.bin | $(TRIM_SIG) > repro-got.txt
hexdump -C check-fw.bin | $(TRIM_SIG) > repro-want.txt
diff repro-got.txt repro-want.txt
@echo ""
@echo "SUCCESS. "
@echo ""
@echo "You have built a bit-for-bit identical copy of Coldcard firmware for v$(VERSION_STRING)"
endif
# EOF

Some files were not shown because too many files have changed in this diff Show More