aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-28 11:45:11 +0100
committercirakg <cirakg@cirakg.xyz>2026-10-28 11:45:11 +0100
commit09ef21af399a60ceb2e45526fe1a7dd94c53819c (patch)
tree7992abb501f75dfe393b2bfd424227d1fc06b5b6
parent3096eb7d366683caec24ed5d0a70ac5dae6f624d (diff)
chore: prepare repository for open-source release
-rwxr-xr-x.githooks/pre-commit12
-rw-r--r--.gitignore34
-rw-r--r--.gitleaks.toml31
-rw-r--r--CONTRIBUTING.md37
-rw-r--r--LICENSE674
-rw-r--r--LICENSES/pdf417gen.txt20
-rw-r--r--LICENSES/segno.txt27
-rw-r--r--README.md1087
-rw-r--r--SECURITY.md15
-rw-r--r--THIRD_PARTY_NOTICES.md16
-rwxr-xr-xdocker/scripts/package.sh6
-rw-r--r--docs/DEVELOPER_GUIDE.md10
-rw-r--r--docs/PERFORMANCE.md12
-rw-r--r--docs/PROJECT_PLAN.md1076
-rw-r--r--docs/SECURITY.md18
-rw-r--r--docs/SETTINGS_SPIKE.md12
-rw-r--r--docs/TESTING.md18
-rw-r--r--docs/USER_GUIDE.md2
-rw-r--r--resources/pdf417/codes0.json2
-rw-r--r--resources/pdf417/codes1.json2
-rw-r--r--resources/pdf417/codes2.json2
-rw-r--r--resources/pdf417/ec.json2
-rw-r--r--resources/pdf417/text.json2
-rw-r--r--source/codes/QrTables.mc1
-rw-r--r--tools/gen_pdf417_tables.py5
-rw-r--r--tools/gen_qr_tables.py1
-rw-r--r--tools/lint_project.py4
-rw-r--r--tools/tests/test_lint_project.py2
28 files changed, 2033 insertions, 1097 deletions
diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100755
index 0000000..d104d78
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,12 @@
+#!/usr/bin/env sh
+# Author: cirakg & claude ai
+# Summary: Scan staged changes with Gitleaks before committing.
+set -eu
+
+if ! command -v gitleaks >/dev/null 2>&1; then
+ echo "Gitleaks is required by this hook. See CONTRIBUTING.md for installation." >&2
+ exit 1
+fi
+
+repo_root=$(git rev-parse --show-toplevel)
+exec gitleaks git --pre-commit --staged --redact --no-banner --config "$repo_root/.gitleaks.toml" "$repo_root"
diff --git a/.gitignore b/.gitignore
index 82bb7f7..18e7f87 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,37 @@ __pycache__/
# Node modules are installed only inside the docs image.
node_modules/
+
+# Local credentials, exports and editor/agent state.
+.env
+.env.*
+!.env.example
+*.key
+*.p12
+*.pfx
+*.jks
+*.keystore
+id_rsa*
+id_ed25519*
+/.aws/
+/.ssh/
+/.garmin/
+/.Garmin/
+/.agents/
+/.claude/
+/.codex/
+/.idea/
+/.vscode/
+/.venv/
+/venv/
+/.secrets/
+/private/
+*aegis*.json
+*aegis*.json.*
+*aegis*.zip
+*aegis*.zip.*
+*.DAT
+*.SET
+*.STR
+.DS_Store
+*~
diff --git a/.gitleaks.toml b/.gitleaks.toml
new file mode 100644
index 0000000..fe41acf
--- /dev/null
+++ b/.gitleaks.toml
@@ -0,0 +1,31 @@
+# Author: cirakg & claude ai
+# Summary: Default secret detectors with narrowly scoped, reviewed test-value exceptions.
+title = "OTP Wallet secret scanning"
+minVersion = "v8.25.0"
+
+[extend]
+useDefault = true
+
+[[allowlists]]
+description = "Published OTP test keys in hand-written fixtures"
+targetRules = ["generic-api-key"]
+condition = "AND"
+regexTarget = "secret"
+paths = ['''(?:^|/)source/app/Fixtures\.mc$''', '''(?:^|/)test/import/ImportSupport\.mc$''', '''(?:^|/)test/import/OtpAuthTest\.mc$''', '''(?:^|/)tools/gen_fuzz_corpus\.py$''']
+regexes = ['''^(?:GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ|MFRGGZDFMZTWQ2LKNNWG23TPOBYXE43U)$''']
+
+[[allowlists]]
+description = "Deterministic generated test values in test/data/FuzzCorpus.mc"
+targetRules = ["generic-api-key"]
+condition = "AND"
+regexTarget = "secret"
+paths = ['''(?:^|/)test/data/FuzzCorpus\.mc$''']
+regexes = ['''^(?:GEZDGDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ|GEZDGNBVGY3TQOJQGAZDGNBVGY3TQOJQ|GEZDGNBVGY3TQOJQGE|GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJ|GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ|GEZDGNBVGY3TQOJQGEZDGNBWaVGY3TQOJQ|GEZDGNBVGY3TQOJQGEnullZDGNBVGY3TQOJQ|GEZDGNBVOJQGEZDGNBVGY3TQOJ|GEZDGsNBVGY3TQOJQGEZDGNBVGY3TQOJQ)$''']
+
+[[allowlists]]
+description = "Deterministic generated test values in test/data/Vectors.mc"
+targetRules = ["generic-api-key"]
+condition = "AND"
+regexTarget = "secret"
+paths = ['''(?:^|/)test/data/Vectors\.mc$''']
+regexes = ['''^(?:60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54|80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598|aa4ae5e15272d00e95705637ce8a3b55ed402112)$''']
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..99a1bf5
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,37 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Development checks, safe test data and contribution licensing.
+-->
+# Contributing
+
+Bug reports and focused patches are welcome. Include the app version, watch model, reproduction steps, and expected and actual behavior. Use fabricated entries in examples, logs and screenshots. See [SECURITY.md](SECURITY.md) for vulnerability reports.
+
+## Development
+
+Follow the Docker setup in the [README](README.md) and the [developer guide](docs/DEVELOPER_GUIDE.md). Use `make test-tools` for Python tooling changes, `make test` for application tests, `make lint` for static checks, and `make docs` for documentation. UI changes should also pass `make screenshots`; review image differences before approving new golden images. Before a release, run `make ci ALL=1` for the full device matrix. Routine checks default to `fr70`.
+
+Describe what changed and how you verified it. Keep expensive work in resumable jobs and preserve the app's no-network-permissions design. The [project plan](docs/PROJECT_PLAN.md) preserves the original numbered design sections referenced in source comments and historical changelog entries.
+
+## Secrets and private data
+
+Never commit signing keys, Garmin credentials, SDK/device downloads, real OTP seeds, authenticator exports, watch storage dumps, or screenshots containing real accounts or barcodes. `.gitignore` covers common filenames, but cannot identify every private file. Keep personal test data outside this checkout. The checked-in fixtures use published RFC keys and fabricated accounts; they must never be used for real accounts.
+
+Install [Gitleaks](https://github.com/gitleaks/gitleaks) v8.25.0 or newer for secret scanning. Scan all reachable history before publishing:
+
+```sh
+gitleaks git --log-opts=--all --redact --no-banner .
+```
+
+Optionally enable the staged-file check for each commit (requires Gitleaks on the host):
+
+```sh
+git config core.hooksPath .githooks
+```
+
+The checked-in configuration extends Gitleaks' default detectors and allows only specific reviewed test values in specific fixture files. Review new findings; do not suppress a whole test folder. Git history also preserves deleted data and commit author emails. If a real credential is committed, revoke or rotate it before arranging history cleanup.
+
+## Licensing
+
+By submitting a contribution, you agree to license your original work under GPL-3.0-only, as described in [LICENSE](LICENSE). Preserve upstream notices when using third-party material and update [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). Garmin's SDK and device files have their own terms and must not be added to the repository.
+
+When distributing a binary, include the license and upstream notices copied into `build/package/` by `make package`, and provide the corresponding source, including build scripts and any modifications. A Store upload alone does not supply that source: publish the matching source version and make its location available to recipients.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ 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>.
diff --git a/LICENSES/pdf417gen.txt b/LICENSES/pdf417gen.txt
new file mode 100644
index 0000000..dc1bdd1
--- /dev/null
+++ b/LICENSES/pdf417gen.txt
@@ -0,0 +1,20 @@
+PDF417-Py License
+
+Copyright (c) 2016 Ivan Habunek <ivan.habunek@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/LICENSES/segno.txt b/LICENSES/segno.txt
new file mode 100644
index 0000000..b80a6f0
--- /dev/null
+++ b/LICENSES/segno.txt
@@ -0,0 +1,27 @@
+Copyright (c) 2016 - 2025, Lars Heuer
+All rights reserved.
+
+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 the copyright holder 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.
diff --git a/README.md b/README.md
index e21dbe2..44066ff 100644
--- a/README.md
+++ b/README.md
@@ -1,1074 +1,53 @@
<!--
Author: cirakg & claude ai
- Summary: Project overview, design and build plan for the OTP Wallet Connect IQ app.
+ Summary: Public overview, build instructions, security limits and licensing.
-->
# OTP Wallet for Connect IQ
-A Garmin Connect IQ watch app that shows one-time passwords (TOTP, HOTP, and optional Steam Guard codes) and stores barcodes and QR codes (loyalty cards, tickets) to show at a scanner. Everything lives encrypted on the watch. There is no cloud, no backend and no analytics, and the app requests no permissions, so it cannot use the network.
+OTP Wallet is a Garmin Connect IQ watch app for displaying TOTP, HOTP, and Steam Guard codes, and showing saved barcodes and QR codes at a scanner. Entries are added and managed through Garmin Connect app settings. The app supports QR, Data Matrix, PDF417, Aztec, and common linear barcode formats; device size and scanner compatibility can limit what is readable.
-> **Status: 1.0.0 released (2026-10-28)** after two real-watch rounds on the Forerunner 70 ([docs/TESTING.md](docs/TESTING.md#results)). The Store listing text and images are in [docs/STORE_LISTING.md](docs/STORE_LISTING.md). The history below is kept as written at M8.
->
-> **M8 (polish, performance, release candidate) done in Docker as version 0.8.0.** The plan below was approved on 2026-10-18. M0 built the Docker toolchain and pipeline, M2 the crypto and OTP core, M3 the encrypted vault ([docs/SECURITY.md](docs/SECURITY.md)), M4 the screens, M5 import (`otpauth://` and Aegis JSON via the phone settings), M6 all 12 barcode symbologies and M7 the phone settings forms. M8 added:
->
-> - **The favourite code in the glance** (opt-in), and favourite marks in Manage entry.
-> - **Brightness** for barcodes: full brightness on AMOLED watches, the backlight on MIP watches, for about a minute.
-> - **Memory and speed:** a 100-entry import peaks at 45 % of the Forerunner 255S's memory instead of 56 %; QR mask scoring on packed rows; two job steps per tick on the 768 KB watches.
-> - **Watchdog safety:** every 2D encoder now handles the largest inputs the app accepts without being stopped by the watchdog, proven by `make watchdog` at twice the normal work per event.
-> - **Docs:** the [user guide](docs/USER_GUIDE.md) with the [known limitations](docs/USER_GUIDE.md#known-limitations).
-> - **Your next steps:** [docs/YOUR_STEPS.md](docs/YOUR_STEPS.md), everything left for you before 1.0.0, in order.
->
-> Waiting for you: the settings spike (`make spike`, [docs/SETTINGS_SPIKE.md](docs/SETTINGS_SPIKE.md)) and the [real-watch checklist](docs/TESTING.md#real-watch-checklist) with the 0.8.0 beta. The Phase 0 research, with sources, is in [docs/FEASIBILITY.md](docs/FEASIBILITY.md); the daily workflow is in [docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md).
+The project is released as version 1.0.0. The app has no network permissions or backend. Entry records are encrypted in watch storage. A PIN can protect selected entries, but it is four digits and should not be treated as strong protection against someone with copied watch data. Unlocked entries use a device key stored with the app data. Garmin Connect settings are handled by Garmin; do not assume secrets pasted there are private from Garmin. There is no export or backup feature, so keep a separate secure backup of your source authenticator data.
-"OTP Wallet" is a working title.
+## Features and limitations
-## Contents
+- TOTP and HOTP with SHA-1, SHA-256, or SHA-512, plus Steam Guard codes.
+- Per-entry PIN protection, session locking, and optional encrypted entry names.
+- Manual entry, `otpauth://` import, and unencrypted Aegis JSON import through Garmin Connect settings.
+- Barcode and QR code display; codes are generated on the watch.
+- No watch camera, encrypted Aegis import, or export/backup from the app.
+- Phone settings require a Connect IQ Store installation; sideloaded builds are for development and testing.
-1. [Scope](#1-scope)
-2. [Decisions and open questions](#2-decisions-and-open-questions)
-3. [Feasibility findings](#3-feasibility-findings)
-4. [How the app reaches your watch](#4-how-the-app-reaches-your-watch)
-5. [Architecture](#5-architecture)
-6. [Data model](#6-data-model)
-7. [Security design](#7-security-design)
-8. [Threat model](#8-threat-model)
-9. [Time handling](#9-time-handling)
-10. [Settings design](#10-settings-design)
-11. [Import design](#11-import-design)
-12. [Barcodes and QR codes](#12-barcodes-and-qr-codes)
-13. [UX design](#13-ux-design)
-14. [Target devices and API levels](#14-target-devices-and-api-levels)
-15. [Docker and sandbox](#15-docker-and-sandbox)
-16. [Build plan](#16-build-plan)
-17. [Test plan](#17-test-plan)
-18. [Performance budgets](#18-performance-budgets)
-19. [Risks and mitigations](#19-risks-and-mitigations)
-20. [Documentation](#20-documentation)
-21. [Repository layout](#21-repository-layout)
-22. [Versioning](#22-versioning)
+See the [user guide](docs/USER_GUIDE.md) for setup and use, [security notes](docs/SECURITY.md) for storage details and limits, and [testing results](docs/TESTING.md) for current verification and known limitations. Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
-## 1. Scope
+## Build and test
-### What it will do
+Requirements: Docker with Buildx and GNU Make. The Garmin Connect IQ SDK is downloaded separately after you accept Garmin's license and sign in.
-- **One-time passwords**: TOTP (RFC 6238) and HOTP (RFC 4226) with SHA-1, SHA-256 or SHA-512, 6 to 8 digits, and any period from 1 to 300 seconds (default 30). Steam Guard codes are a separate, optional entry type.
-- **Barcodes and QR codes**: QR, Code 128, Code 39, EAN-13, EAN-8, UPC-A, UPC-E, ITF, Codabar, Data Matrix, PDF417 and Aztec. The watch stores the text and the symbology, then encodes and draws the code itself. A symbology ships only once it passes the checks in [section 12.4](#124-how-each-symbology-is-proven-scannable). Anything that doesn't pass is listed as deferred.
-- **Per-entry lock**: each entry has its own "Require PIN to display" setting. One app PIN unlocks a session.
-- **Encryption at rest** for every entry.
-- **Manual entry from your phone**: you add, edit, reorder and delete entries in the Connect IQ app settings, using an "add" form for each kind and a "manage" form.
-- **Import**: paste `otpauth://` URIs or an unencrypted Aegis JSON export into the app settings.
-- **Simple, fast watch UI**: a list of entries, a big code with a countdown ring, tap to advance an HOTP counter. It works with touch screens and with buttons only.
-
-### What it won't do
-
-| Not supported | Why |
-| --- | --- |
-| Unlocking with the watch's own PIN or passcode | Connect IQ has no API for it |
-| Encrypted Aegis exports | Decrypting one needs 32 MiB of RAM; watch apps get under 0.8 MB. You chose plain JSON import over an off-watch converter (decision D2) |
-| Aegis icons and notes, mOTP and Yandex entries | The watch doesn't decode images, notes may hold private text, and mOTP/Yandex weren't requested. They're skipped with a message |
-| Everyday use as a sideloaded app | Sideloaded apps get no phone settings, so you couldn't add entries. Sideloaded builds are for testing only |
-| Exporting or backing up entries from the watch | Deliberate: secrets only flow *into* the watch. Your Aegis export and your own records are the backup |
-| Scanning codes with the watch | There is no camera |
-| Very long linear codes and large 2D codes on small screens | They don't fit at a scannable size. The watch tells you instead of drawing an unreadable code |
-| Strong protection of locked entries if someone copies the data off the watch | A short PIN can be guessed offline ([section 8](#8-threat-model)) |
-| Any network feature | The app requests no permissions |
-
-## 2. Decisions and open questions
-
-Decided on 2026-10-18:
-
-| # | Decision | Choice |
-| --- | --- | --- |
-| D1 | PIN policy | Session unlock: enter the PIN once per session. Everything re-locks when you leave the app or after an inactivity timeout |
-| D2 | Import | Paste `otpauth://` URIs or an unencrypted Aegis JSON export into the settings; the watch parses them. No converter tool, no encrypted Aegis import |
-| D3 | Manual-entry layout | Inbox forms ("Add OTP", "Add code") plus a "Manage entry" form |
-| D4 | Targets | Watches with API 5.0 or newer and at least 512 KB for apps |
-
-Also settled on 2026-10-18, after this plan was approved:
-
-| # | Question | Answer |
-| --- | --- | --- |
-| D5 | Docker access | You joined the `docker` group |
-| Q1 | Test watch | Forerunner 70 (2026): API 6.0, 390×390 AMOLED, touch plus 5 buttons, 768 KB. It's one of the simulator test devices |
-| Q2 | Phone | Android, so the iPhone `&` and `\` problem doesn't affect you |
-
-Still open:
-
-| # | Question | Needed by |
-| --- | --- | --- |
-| D6 | Your Garmin account: accept the SDK licence and log in inside the container (`make sdk`); create a developer account for the beta | Phase 2 and M1 |
-
-## 3. Feasibility findings
-
-A summary. The details, evidence and sources are in [docs/FEASIBILITY.md](docs/FEASIBILITY.md).
-
-| # | Question | Verdict | Consequence |
-| --- | --- | --- | --- |
-| 1 | Unlock entries with the Garmin device PIN | Not possible | App PIN with session unlock (D1). Garmin's system passcode, where a watch has one, shuts apps down when it locks and complements the app lock |
-| 2 | SHA-1/256/512, HMAC, PBKDF2, AES on the watch | Partially | Native: SHA-1, SHA-256, AES-ECB/CBC, HMAC-SHA256, random bytes. Built from their standards in Monkey C: HMAC-SHA1, SHA-512, PBKDF2, HKDF, encrypt-then-MAC |
-| 3 | Import and manual entry through app settings | Partially | Text fields only, no file upload. Settings only work for Store installs, so delivery is a private beta. Array settings are broken (CIQQA-4751), hence the inbox design (D3) |
-| 4 | Storage limits | Partially | Garmin's pages disagree. Design for at most 8 KB per value and about 100 KB in total |
-| 5 | Memory | Supported on the targets | 512 to 768 KB per app on API 5.x and 6.x watches |
-| 6 | Barcode rendering | Partially | All 12 symbologies can be drawn. Big codes don't fit small round screens. Scanner type matters |
-| 7 | App type | Supported | Device app with a glance |
-| 8 | Privacy of settings | Partially (undocumented) | Treat anything typed into settings as stored by Garmin. A consequence of D2 and D3 |
-| 9 | Encrypted Aegis export on the watch | Not possible | Out of scope (D2) |
-| 10 | TOTP after 2038 | Partially | The clock value is 32-bit. It's reinterpreted as unsigned, which is correct until 2106 |
-| 11 | Headless SDK, simulator and tests in Docker | Partially | Your Garmin login is needed once for device files. Ubuntu 22.04 base image |
-
-## 4. How the app reaches your watch
-
-Garmin's app settings only work for apps installed from the Connect IQ Store, so:
-
-1. `make package` builds a signed `.iq` file in Docker.
-2. You upload it on Garmin's developer website as a **beta app**, which only your account can see. Every new version is a new upload.
-3. You install it on your watch from the Connect IQ Store app.
-4. You manage entries in the Garmin Connect app on your phone: your watch, then Connect IQ apps, then OTP Wallet, then Settings.
-
-Development and automated tests use the simulator in Docker. For speed and rendering checks on a real watch, a sideloaded debug build contains fake fixture data and needs no settings.
-
-## 5. Architecture
-
-```mermaid
-flowchart TB
- subgraph Phone["Phone: Garmin Connect app settings"]
- F["Add OTP / Add code / Manage entry / Import / Options"]
- end
- subgraph Watch["Watch app (Monkey C)"]
- APP["app: lifecycle, glance entry"]
- UI["ui: list, OTP, code, PIN, menu, results, glance"]
- SET["settings: form reader, inbox, manage, options, status"]
- IMP["import: otpauth, Aegis JSON, JSON tokenizer"]
- ST["storage: vault, records, keys, session, lockout, migrations"]
- OTP["otp: Base32, HOTP, TOTP, Steam"]
- COD["codes: encoders, Reed-Solomon, validators, renderer"]
- CR["crypto: hashes, HMAC, SHA-512, PBKDF2, HKDF, AES-CBC + HMAC"]
- UT["util: clock, jobs, bytes, errors, debug log"]
- end
- F -- "values, via Garmin" --> SET
- APP --> UI
- APP --> SET
- SET --> IMP
- SET --> ST
- SET --> COD
- IMP --> ST
- UI --> ST
- UI --> OTP
- UI --> COD
- ST --> CR
- OTP --> CR
- SET --> UT
- ST --> UT
- UI --> UT
-```
-
-| Module | Responsibility |
-| --- | --- |
-| `app/` | `AppBase`: lifecycle, initial view, glance view, `onSettingsChanged`, locking in `onStop` |
-| `crypto/` | Wrappers around the native hashes, HMAC (RFC 2104), SHA-512 (FIPS 180-4), PBKDF2 (RFC 8018), HKDF (RFC 5869), AES-CBC with PKCS#7 padding, encrypt-then-MAC records, constant-time comparison, zeroing, random bytes, start-up self-test |
-| `otp/` | Base32, HOTP, TOTP, Steam, parameter validation |
-| `codes/` | Bit-matrix and bar-pattern types, Reed–Solomon over GF(2^m) and GF(929), one encoder per symbology, validators, layout and renderer |
-| `storage/` | Vault (records and index), record codec, key manager (device key, locked master key, PIN wrapping), session, lockout, migrations, quotas, pending queue |
-| `import/` | `otpauth://` parser, streaming JSON tokenizer, Aegis reader (database versions 1 to 3), import service (duplicates, partial imports, multi-part buffer, report) |
-| `settings/` | Reads settings into typed forms; processes the inbox, manage, import and options forms; writes status fields and the catalog |
-| `ui/` | Views and input delegates; layouts for round, rectangular and semi-octagon screens |
-| `util/` | Clock (UTC, unsigned widening, offset, trusted-clock check), job scheduler, byte and string helpers, error types, debug-only log |
-
-Design rules:
-
-- **Thin UI.** Views draw view models. The logic lives in plain modules that tests can run without a screen.
-- **No network, no permissions.** Anyone can check this in `manifest.xml`.
-- **No watchdog trips.** Long work (key derivation, SHA-512 batches, 2D encoding, large imports) runs as resumable jobs on a timer, and the UI shows progress.
-- **No allocations in hot paths.** The 1 Hz countdown redraw reuses precomputed strings and geometry. Each code is computed once per period.
-- **Caches**: encoded barcode matrices (encrypted, in storage), drawn bitmaps per screen, and computed codes with the time they expire.
-- **Secrets are `ByteArray`s**, zeroed after use. The only secret-derived `String` is the short-lived code on screen.
-- **Debug and test code is excluded from release builds**: debug-only code (fixtures, performance overlay, logging) is annotated `(:debug)`, and tests are annotated `(:test)`.
-- **Code style**: heavy comments (purpose, inputs, outputs, errors, security notes), small functions, named constants, and every external input validated.
-
-## 6. Data model
-
-### 6.1 Entry fields
-
-| Field | Applies to | Where it's stored | Notes |
-| --- | --- | --- | --- |
-| `id` | all | header | 4 characters from `23456789ABCDEFGHJKMNPQRSTVWXYZ`, random and unique. Shown on the watch and used by the Manage form |
-| `kind` | all | header | OTP or code |
-| `type` | OTP | header | TOTP, HOTP or Steam |
-| `symbology` | code | header | One of the 12 |
-| `name` | all | header, or encrypted when "Encrypt names" is on | The issuer for OTP entries, the card name for codes |
-| `account` | OTP | same as `name` | The account label |
-| `group` | all | same as `name` | Optional |
-| `position` | all | index | Display order |
-| `locked` | all | header | "Require PIN to display" |
-| `source` | all | header | Manual or import. Used by "Replace imported entries" |
-| `favorite` | all | header | Comes from Aegis. The first favourite unlocked TOTP entry can appear in the glance |
-| `counter` | HOTP | header, covered by the record's MAC | Changes on every "next". Not secret |
-| `algorithm`, `digits`, `period` | OTP | encrypted | Not secret, but kept next to the secret so tampering is detected |
-| `secret` | OTP | encrypted | Raw bytes, decoded from Base32 on input |
-| `payload` | code | encrypted | UTF-8 text |
-| `options` | code | encrypted | Error-correction level, human-readable text, check-digit mode |
-| `created` | all | header | Used in reports |
-
-### 6.2 Storage layout (`Application.Storage`)
-
-| Key | Content | Protected by |
-| --- | --- | --- |
-| `m` | Metadata: schema version, entry index (the order of IDs), pending IDs | Not secret |
-| `dk` | Device key, 32 random bytes | Stored as is ([7.4](#74-what-stays-readable)) |
-| `w` | KDF salt, iteration count and the wrapped locked master key | The PIN key |
-| `lk` | Wrong-PIN count and delay | Not secret |
-| `e.<id>` | One entry record | Device key, or the locked master key for locked entries |
-| `x.<id>` | Cached barcode matrix for a code entry | The same key as its entry |
-| `p.<id>` | Pending locked entry, waiting for your PIN | Device key ([7.7](#77-pending-locked-entries)) |
-| `imp`, `imp.<n>` | Partial import text, kept between syncs, in chunks below 8 KB | Device key |
-| `q` | Phone changes to locked entries, waiting for your PIN ([7.9](#79-integrity-of-changes-coming-from-the-phone)) | Device key |
-| `qm.<n>` | The new secret or contents of a waiting "Replace", stored apart from `q` | Device key |
-| `res`, `fh`, `cs`, `st` | Latest settings results; keyed hashes of applied forms; signature of the last About update; date of the last self-test | Not secret |
-
-Limits: at most 8 KB per value and about 100 KB in total (the stricter of Garmin's two documented figures). M3 measured 239 bytes for a typical OTP record, so the entry cap stays at 150 (byte formats in [docs/SECURITY.md](docs/SECURITY.md)). A full store produces a clear message, never a crash.
-
-### 6.3 Schema versions and migration
-
-- `m.schema` holds the data version. Each record also carries its own format version.
-- At start-up: an older schema runs migrations step by step (v1 to v2 to v3...). Each migration is a small, idempotent, unit-tested function. Before overwriting anything, a record is written under a temporary key and the index is switched to it, so a crash or power loss mid-migration loses nothing.
-- A newer schema (after a downgrade) is never modified. The app shows "Data from a newer version — update the app".
-- Unreadable or damaged records show as "Damaged entry K7F2 — re-add it" instead of crashing.
-- Garmin doesn't document what happens to storage on app updates. The manual test checklist verifies that an update keeps your data.
-
-## 7. Security design
-
-### 7.1 Keys
-
-```mermaid
-flowchart TD
- PIN["PIN typed on the watch"] --> KDF["PBKDF2-HMAC-SHA256: random salt, iterations calibrated to about 1 s"]
- KDF --> PK["PIN key: RAM only"]
- PK -- "unwraps: AES-CBC + HMAC-SHA256" --> LMK["Locked master key: 32 random bytes, stored wrapped"]
- DK["Device key: 32 random bytes, stored"]
- LMK -- "HKDF" --> LK["encryption key + MAC key"]
- DK -- "HKDF" --> DKK["encryption key + MAC key"]
- LK --> LR["Locked entries"]
- DKK --> UR["Unlocked entries, pending items, import buffer"]
-```
-
-### 7.2 PIN and key derivation
-
-- The PIN is exactly 4 digits, entered like the watch's own lock screen: the 4th digit submits it, with no OK key (changed after the real-watch test, 0.9.0). A PIN set by 0.8.0 or earlier (4 to 10 digits) still unlocks; the watch then asks for a new 4-digit PIN.
-- `PIN key = PBKDF2-HMAC-SHA256(PIN digits as ASCII, 16-byte random salt, N iterations, 32 bytes)`.
-- N is calibrated on your watch when you create or change the PIN, so one derivation takes about 1 s. It runs as a resumable job and is stored next to the salt. The lower and upper bounds for N are set from the M2 measurements.
-- The PIN key unwraps the locked master key. A correct PIN is exactly "the wrap's MAC verifies". No PIN hash is stored anywhere.
-- Changing the PIN only re-wraps the master key. Entries are not re-encrypted.
-
-### 7.3 Record encryption
-
-```text
-keys: Kenc, Kmac = HKDF-SHA256(master key, info = "OTPW enc v1" / "OTPW mac v1")
-record: v = 1 record format version
- h = TLV(header fields) readable metadata (6.1)
- kr = 0 | 1 0 = device key, 1 = locked master key
- iv = 16 random bytes new on every write
- ct = AES-CBC(Kenc, iv, PKCS7(TLV(sensitive fields)))
- tag = HMAC-SHA256(Kmac, "OTPW-REC-1" || id || v || kr || h || iv || ct)
-```
-
-- **Encrypt-then-MAC.** The tag is checked in constant time *before* anything is decrypted, so tampering anywhere (header, IV, ciphertext) is detected.
-- **TLV** is a small deterministic byte encoding, so the MAC input is canonical.
-- **AES-256** if every target passes the M2 tests with it, otherwise AES-128. The PIN, not the cipher, is the weakest link either way.
-- **The PIN wrap** uses the same scheme, with its own keys derived from the PIN key and the label "OTPW-WRAP-1". The salt and iteration count are included in the MAC.
-- Only standard constructions are used: PBKDF2, HKDF, AES-CBC, HMAC, encrypt-then-MAC. Each is tested against official vectors (section 17.2).
-
-### 7.4 What stays readable
-
-- **The device key.** It must be on the watch so unlocked entries open without a PIN. Unlocked entries are therefore only obscured against someone reading raw storage. Garmin says app storage isn't accessible to users, and that Store-signed apps get an encrypted object store. The design doesn't rely on either.
-- **Record headers**: ID, kind, type or symbology, locked flag, HOTP counter, position, creation date, and names unless "Encrypt names" is on. This lets the list draw without decrypting anything.
-- **"Encrypt names"** (off by default) moves name, account and group into the ciphertext. Unlocked entries' names are then decrypted at start-up with the device key (the cost is measured), and locked entries show as "Locked entry K7F2" until you unlock.
-
-### 7.5 Session and auto-lock
-
-- **Unlocking**: PIN, then PIN key, then the master key is unwrapped and held in RAM.
-- **Locking happens** when you leave the app (`onStop`), after the inactivity timeout, or through "Lock now" in the menu. The timeout setting offers Immediately, 15 s, 30 s, 1 min (default), 2 min and 5 min. "Immediately" locks as soon as you leave a locked entry, which gives you the "PIN every time" behaviour whenever you want it.
-- **On lock**: the master key and derived keys are zeroed and dropped, cached codes and bitmaps of locked entries are discarded, and any open locked entry closes back to the list.
-
-### 7.6 Wrong PINs
-
-- The failure count and the next allowed attempt time are written to storage *before* each check (`Storage.setValue` writes to disk immediately) and reset on success. Restarting the app or the watch doesn't help an attacker.
-- Four free attempts. From the 5th failure, delays of 30 s, 1 min, 2 min, 4 min and so on, capped at 1 hour.
-- Delays are timed with the watch's trusted clock (`Time.CURRENT_TIME_RTC`, synced from GPS; the user can't change it). If that clock isn't synced, the delay counts app uptime instead and starts again in full after a restart: never shorter, sometimes longer.
-- The optional "Erase locked entries after 10 wrong PINs" setting is off by default. It deletes the wrapped master key, all locked entries and all pending items.
-- Forgotten PIN: "Reset PIN" erases the locked entries (they can't be recovered, by design) and sets a new PIN.
-
-### 7.7 Pending locked entries
-
-A locked entry can arrive from settings or an import while the watch is locked, or before any PIN exists. It can't be encrypted with the master key yet. It's stored under the device key as **pending**, which is no worse than the plain text that just came through settings, and hidden from the list. On the next open, the app asks for the PIN (or to create one), re-encrypts the entry under the master key, and zeroes and deletes the pending copy.
-
-### 7.8 Memory hygiene
-
-- Keys and decrypted secrets are `ByteArray`s, overwritten with zeros right after use. An unlocked session holds only the master key.
-- To show a code, the app decrypts the secret, computes the code and zeroes the secret. Only the short code string is kept, together with the time it expires.
-- **Limit**: Monkey C gives no guarantee that the VM didn't copy data (for example inside native crypto objects), and the system doesn't clear freed memory. This is best effort, and documented as such.
-- **No secrets in logs.** Release builds contain no logging. Debug logging goes through one function that refuses byte arrays and secret fields, and a lint rule enforces it.
-
-### 7.9 Integrity of changes coming from the phone
-
-Anyone who controls your phone or Garmin account can change the settings. So:
-
-- Actions on locked entries (edit, replace secret, unlock, set the counter, delete) wait in a queue until you confirm them with the PIN on the watch, even if the watch happens to be unlocked. The menu lists them under "Phone changes", where you confirm (the PIN is always asked) or reject them. "Require PIN" waits too while the watch is locked, because it needs the master key; it applies by itself at the next PIN entry, and what it did is listed under the menu's results.
-- A replacement secret or code contents is checked when it arrives: an invalid one is refused at once, and only a valid one waits.
-- Deleting or editing *unlocked* entries applies directly, and the watch shows what changed.
-- New entries always apply, and they're visible in the list.
-
-### 7.10 Start-up self-test
-
-On the first start after an install or update, and then once a day, the app runs known-answer tests: SHA-1, SHA-256 and SHA-512, HMAC, AES, and PBKDF2 with a low iteration count. If one fails, the app shows "Crypto self-test failed on this watch" and refuses to show codes rather than show wrong ones.
-
-## 8. Threat model
-
-**Assets**: OTP secrets, barcode contents, the PIN, entry names.
-
-| Scenario | Protected? | Notes |
-| --- | --- | --- |
-| Someone picks up your watch while the app is closed | Locked entries: yes. Unlocked entries: no, by design | Lock what matters. Turn on Garmin's system passcode if your watch has one |
-| Someone grabs the watch during an unlocked session | Until auto-lock (default: 60 s without input) | Choose a shorter timeout, or "Immediately" |
-| Lost or stolen watch, attacker uses the screen | Locked entries: yes | Lockout delays, optional erase |
-| Lost or stolen watch, attacker extracts the storage | Unlocked entries: no. Locked entries: only as strong as the PIN | 4 digits means 10,000 guesses. Each costs one ~1 s PBKDF2 run on the watch but microseconds on a PC, so a copied storage falls in seconds. The PIN protects against someone using the watch (delays after wrong PINs, optional erase), not against a storage copy. Garmin's object-store encryption for Store apps may stop the copy; it's undocumented, so it isn't relied on. The debug build's files were visible over USB on the Forerunner 70 (part 4 of docs/YOUR_STEPS.md) |
-| Garmin's servers and the Garmin Connect app | No, for anything typed or pasted into settings | A consequence of D2 and D3: secrets travel in the clear. The watch clears the fields after reading them, but Garmin may keep copies |
-| Malware on your phone (clipboard, accessibility services) | No | It can read what you paste. Clear the clipboard and delete the Aegis export after importing |
-| Someone controlling your phone or Garmin account changes settings | Partly | They can add, or delete unlocked, entries. Changes to locked entries need your PIN on the watch ([7.9](#79-integrity-of-changes-coming-from-the-phone)) |
-| Other Connect IQ apps on the watch | Yes | Garmin sandboxes each app's storage |
-| Network attackers | Yes | No network permission |
-| Shoulder surfing | Partly | A code is visible by nature. PIN digits show as dots |
-| Changing the watch clock to harvest future codes | Partly | Locked entries still need an unlocked session. The app warns when the clock disagrees with the trusted clock |
-| Compromised build tools | Partly | SDK from Garmin; versions pinned; images pinned by digest; Python packages pinned with hashes |
-| Out of scope | | Compromised watch firmware, RAM forensics on a running watch, hardware attacks, Garmin itself |
-
-## 9. Time handling
-
-- **Codes use UTC.** `Time.now()` gives UTC seconds, plus your offset setting (−120 to +120 s, default 0) for skew.
-- **2038**: Garmin stores time as a 32-bit signed integer. The app reinterprets negative values as unsigned 64-bit, which is correct until 2106. The OTP maths uses 64-bit time throughout, so the RFC 6238 test vector for the year 2603 passes.
-- **Trusted clock**: `Time.getCurrentTime({:currentTimeType => Time.CURRENT_TIME_RTC})` is synced from GPS and can't be changed by the user. If it isn't synced, a small clock icon warns that codes may be off. If it differs from the displayed time by more than 30 s, the app warns "Watch time differs from GPS time".
-- **If the time is wrong, codes are wrong.** The app can't fix that, but the warnings and the offset setting make it visible and correctable.
-- **HOTP**: the stored counter is the one whose code is on screen. "Next" saves counter + 1 first, then shows the new code, so a crash never reuses a code. If you also use the same HOTP account elsewhere, the counters drift apart.
-- **Refresh**: on the detail screen, a 1-second timer moves the countdown ring. The code is recomputed only at the period boundary, and in the last 5 seconds the next code is shown as a preview.
-
-## 10. Settings design
-
-### 10.1 Why inbox and manage forms
-
-Garmin's "array settings" (growable lists) are broken today. A failed save resets all of the app's settings (CIQQA-4751). Instead, there is one **Add OTP** form and one **Add code** form that the watch reads and then clears, plus one **Manage entry** form that acts on an entry by its ID. The watch is the database. The forms are how you send it requests.
-
-### 10.2 Groups and fields
-
-#### Add OTP
-
-| Property | Type | Field | Notes |
-| --- | --- | --- | --- |
-| `otpType` | number | list | TOTP, HOTP, Steam |
-| `otpIssuer` | string | alphaNumeric (max 40) | |
-| `otpAccount` | string | alphaNumeric (max 64) | |
-| `otpSecret` | string | password | Base32. Masked on screen only |
-| `otpAlgorithm` | number | list | SHA1, SHA256, SHA512 (ignored for Steam) |
-| `otpDigits` | number | list | 6, 7, 8 (Steam always shows 5 characters) |
-| `otpPeriod` | number | numeric (1 to 300) | TOTP. Steam is always 30 |
-| `otpCounter` | long | numeric (≥ 0) | HOTP starting counter |
-| `otpGroup` | string | alphaNumeric (max 32) | Optional |
-| `otpLocked` | boolean | switch | Require PIN to display |
-| `otpAdd` | boolean | switch | **Add this entry**: the watch only acts when this is on |
-| `otpStatus` | string | read-only | Result, written by the watch |
-
-#### Add code
-
-| Property | Type | Field | Notes |
-| --- | --- | --- | --- |
-| `codeName` | string | alphaNumeric (max 40) | |
-| `codeSymbology` | number | list | The 12 symbologies |
-| `codePayload` | string | alphaNumeric | The code's text or digits |
-| `codeEcLevel` | number | list | Low, Medium (default), Quartile, High. Mapped per symbology (QR L/M/Q/H; Aztec 10/23/36/50 %; PDF417 auto or levels 2/4/5/6) |
-| `codeCheckDigit` | number | list | Add or verify (default), Fix if wrong, Off |
-| `codeShowText` | boolean | switch | Human-readable text under linear codes |
-| `codeGroup` | string | alphaNumeric (max 32) | |
-| `codeLocked` | boolean | switch | Require PIN to display |
-| `codeAdd` | boolean | switch | **Add this entry** |
-| `codeStatus` | string | read-only | |
-
-#### Manage entry
-
-| Property | Type | Field | Notes |
-| --- | --- | --- | --- |
-| `mgId` | string | alphaNumeric | The entry ID shown on the watch, for example `K7F2` (case-insensitive) |
-| `mgAction` | number | list | Edit details, Move to position, Require PIN, Don't require PIN, Replace secret or contents, Set HOTP counter, Delete, Mark as favourite (glance), Remove favourite (M8) |
-| `mgName`, `mgAccount`, `mgGroup` | string | alphaNumeric | Blank means unchanged. `-` clears the group |
-| `mgPosition` | number | numeric (≥ 1) | |
-| `mgSecret` | string | password | New secret (Base32) or new code contents |
-| `mgCounter` | long | numeric | |
-| `mgApply` | boolean | switch | **Apply** |
-| `mgStatus` | string | read-only | |
-
-#### Import
-
-| Property | Type | Field | Notes |
-| --- | --- | --- | --- |
-| `impText1` to `impText8` | string | alphaNumeric | Joined in order. The settings spike (M1) measured 8000 characters per field and 64,000 in total arriving complete |
-| `impMode` | number | list | Append (default), Replace imported entries, Cancel pending import |
-| `impLocked` | boolean | switch | Require PIN for every entry in this import |
-| `impApply` | boolean | switch | **Import now** |
-| `impStatus` | string | read-only | |
-
-#### Options
-
-| Property | Field | Default |
-| --- | --- | --- |
-| `optAutoLock` | list: Immediately, 15 s, 30 s, 1 min, 2 min, 5 min | 1 min |
-| `optWipe` | switch: Erase locked entries after 10 wrong PINs | off |
-| `optTimeOffset` | numeric, −120 to 120 s | 0 |
-| `optCodeSize` | list: Normal, Large, Huge (code font) | Large |
-| `optTheme` | list: Dark, Light (barcodes are always black on white) | Dark |
-| `optGroups` | switch: Show groups | on |
-| `optEncryptNames` | switch | off |
-| `optGlanceCode` | switch: Show the favourite code in the glance | off |
-| `optBrightness` | switch: Full brightness while showing a barcode | on |
-
-**About** (all read-only, written by the watch): `catalog` (entry IDs and names, shortened to fit), `version`, `storageUse`.
-
-### 10.3 Processing rules
-
-1. **When**: from `onSettingsChanged` while the app runs, and at every app start. Processing runs *after* the first screen is drawn, so it never slows start-up.
-2. **Switches**: a form is processed only when its switch is on. Afterwards the watch turns the switch off and clears the form, secret fields first. After a problem only the secret field is cleared, so the other fields can be corrected and sent again.
-3. **Idempotency**: Garmin doesn't document whether the phone shows those cleared values ([FEASIBILITY 3e](docs/FEASIBILITY.md#3e-writing-values-back-unknown)). So the watch also remembers a keyed hash of each of the last 32 applied forms and ignores a form it has already applied. Re-sending the same form does nothing; changing anything makes it a new request. Manage actions use stable IDs and absolute positions, so applying one twice can't hit a different entry.
-4. **Results** go to the form's status field and to a "Settings results" screen on the watch. That screen opens by itself after processing if the app is open, and is always available from the menu.
-5. **Status texts are short and specific**, for example:
- - "Added K7F2 GitHub (alice)"
- - "Secret: '1' is not a Base32 character (position 5)"
- - "EAN-13: check digit should be 7, got 3. Choose 'Fix if wrong' to correct it"
-6. **Order**: Import, then Add OTP, then Add code, then Manage, then Options.
-
-### 10.4 Validation (OTP form)
-
-- **Secret**: Base32 (`A`–`Z`, `2`–`7`, any case). Spaces and hyphens are ignored, and `=` padding is optional. It must decode to 1 to 128 bytes, with a warning below 10 bytes (80 bits). Errors name the character and its position.
-- **Digits**: 6 to 8. Steam always uses 5 characters.
-- **Period**: 1 to 300 s. Steam always uses 30.
-- **Counter**: 0 to 2^63−1.
-- **Algorithm**: SHA1, SHA256 or SHA512. Steam always uses SHA1.
-- **Issuer and account**: at least one must be filled; no control characters.
-
-Code validation, per symbology, is in [section 12.1](#121-symbologies).
-
-### 10.5 XML excerpt
-
-The full files are in `resources/settings/` (written in M7). `make lint` validates them against the SDK's `resources.xsd` and checks that the form, the properties and the strings agree (`tools/check_resources.py`).
-
-```xml
-<!-- resources/settings/properties.xml (excerpt) -->
-<properties>
- <property id="otpType" type="number">0</property>
- <property id="otpIssuer" type="string"></property>
- <property id="otpSecret" type="string"></property>
- <property id="otpDigits" type="number">6</property>
- <property id="otpPeriod" type="number">30</property>
- <property id="otpCounter" type="long">0</property>
- <property id="otpLocked" type="boolean">false</property>
- <property id="otpAdd" type="boolean">false</property>
- <property id="otpStatus" type="string"></property>
-</properties>
-```
-
-```xml
-<!-- resources/settings/settings.xml (excerpt) -->
-<settings>
- <group id="addOtp" title="@Strings.GroupAddOtp" description="@Strings.GroupAddOtpHelp">
- <setting propertyKey="@Properties.otpType" title="@Strings.OtpType">
- <settingConfig type="list">
- <listEntry value="0">@Strings.TypeTotp</listEntry>
- <listEntry value="1">@Strings.TypeHotp</listEntry>
- <listEntry value="2">@Strings.TypeSteam</listEntry>
- </settingConfig>
- </setting>
- <setting propertyKey="@Properties.otpIssuer" title="@Strings.OtpIssuer">
- <settingConfig type="alphaNumeric" maxLength="40" />
- </setting>
- <setting propertyKey="@Properties.otpSecret" title="@Strings.OtpSecret" prompt="@Strings.OtpSecretPrompt">
- <settingConfig type="password" />
- </setting>
- <setting propertyKey="@Properties.otpDigits" title="@Strings.OtpDigits">
- <settingConfig type="list">
- <listEntry value="6">@Strings.Digits6</listEntry>
- <listEntry value="7">@Strings.Digits7</listEntry>
- <listEntry value="8">@Strings.Digits8</listEntry>
- </settingConfig>
- </setting>
- <setting propertyKey="@Properties.otpPeriod" title="@Strings.OtpPeriod">
- <settingConfig type="numeric" min="1" max="300" errorMessage="@Strings.OtpPeriodError" />
- </setting>
- <setting propertyKey="@Properties.otpLocked" title="@Strings.RequirePin">
- <settingConfig type="boolean" />
- </setting>
- <setting propertyKey="@Properties.otpAdd" title="@Strings.AddThisEntry">
- <settingConfig type="boolean" />
- </setting>
- <setting propertyKey="@Properties.otpStatus" title="@Strings.Status">
- <settingConfig type="alphaNumeric" readonly="true" />
- </setting>
- </group>
-</settings>
-```
-
-## 11. Import design
-
-### 11.1 Accepted formats
-
-- **`otpauth://` URIs** for `totp`, `hotp` and `steam`. Paste one or many, separated by spaces or line breaks, or simply run together: a new URI starts at every `otpauth://`.
-- **Aegis unencrypted JSON export**: vault format version 1, database versions 1, 2 and 3. Detected by a leading `{`.
-- **Deferred: Google Authenticator** `otpauth-migration://offline?data=...` exports. This was the stretch goal. M5 ran out of the time set aside for it: the format is reverse-engineered (Base64 and protobuf), and its fixtures would have to be built from scratch. The importer recognises these links and explains that they aren't supported yet. Exporting from Aegis (which imports Google Authenticator) works today.
-
-### 11.2 Getting the text into settings
-
-- **Fields**: the eight import fields are joined in order. If the text is longer than one field accepts, continue in the next one.
-- **Several syncs**: if the JSON is still incomplete after joining, the watch keeps what it got (encrypted under the device key) and reports: "Waiting for more: received 3,214 characters. Paste the next part into the import fields and turn 'Import now' on again." "Cancel pending import" discards it. The buffer is capped at 32 KB and expires after 24 hours. M5 measured an Aegis export at about 190 bytes per entry compact, and 400 to 500 pretty-printed. Exports larger than the cap need to be split into several imports.
-- **Size reality**: pretty-printed Aegis JSON takes about 400 to 500 characters per entry without icons. Icons add several KB each and are the main reason a paste might not fit. An `otpauth://` URI takes about 80 to 200 characters.
-- **Spike results (M1, Android, 2026-10-21)**: one field carried 8000 characters and the eight together 64,000, so the 32 KB buffer, not Garmin Connect, limits one sync. `otpauth://` URIs with `&` save as they are. A test string mixing `< > \ " { } %`, accented letters and an emoji was refused by the phone. **Follow-up (round 2, 2026-10-27):** only the backslash `\` is refused; every other character group and a complete unencrypted Aegis export without backslashes saved, and a real Aegis export was imported on the Forerunner 70. Aegis JSON can be pasted when it has no `\` (Android Aegis writes `/` as `\/` in icons): replace `\/` with `/` or remove icons first (docs/SETTINGS_SPIKE.md).
-- **iPhone**: Garmin Connect on iOS reportedly rejects `&` (present in every URI with parameters) and `\` (used in JSON escapes). The spike ran on Android only. If it's confirmed on iOS, the parser also accepts `;` in place of `&`, and the user guide explains the workaround.
-
-### 11.3 Parsing rules
-
-#### `otpauth://` URIs
-
-- The label is percent-decoded and split on the first `:` (or `%3A`) into issuer and account, with surrounding spaces trimmed.
-- An `issuer` parameter wins over the label's issuer. A mismatch is noted in the report.
-- `secret` is required and follows the Base32 rules in 10.4.
-- Defaults: `algorithm` SHA1, `digits` 6, `period` 30. `counter` is required for `hotp`.
-- A `steam` type, or the `encoder=steam` parameter, means Steam.
-- Unknown parameters (`image`, `color` and so on) are ignored.
-- Errors name the URI's position, for example "URI 3: digits=9 is not supported (6–8)".
-
-#### Aegis JSON
-
-- **Encrypted exports are refused** with a message explaining how to export unencrypted. They're recognised by `header.slots` not being null, or `db` being a string.
-- **Types**: `totp`, `hotp` and `steam` are imported. `motp`, `yandex` and unknown types are skipped and reported.
-- **Fields used**: `name` (the account), `issuer`, `info.secret`, `info.algo`, `info.digits`, `info.period`, `info.counter`, `favorite` and groups (UUIDs resolved through the top-level `groups` list in database version 3, or the older `group` string). If an entry has several groups, the first is used.
-- **Ignored**: `icon`, `icon_mime` and `icon_hash` (skipped without loading them into memory) and `note` (may hold private text).
-- **Robust parsing**: the JSON tokenizer streams over the UTF-8 bytes, handles escapes and `\u` sequences, and enforces depth and length limits. It never builds large intermediate objects, and runs as a resumable job.
-
-### 11.4 Duplicates, partial imports and the report
-
-- **Duplicate**: the same type and the same secret as an existing entry. It's skipped and reported ("duplicate of K7F2").
-- **Same issuer and account, different secret**: imported as a new entry and flagged ("possible replacement for K7F2 — delete the old one with Manage entry").
-- **Partial imports**: every valid entry is stored, and invalid ones are listed with reasons. Entries are stored one at a time, so a crash or power loss leaves only whole entries.
-- **Modes**: Append, or Replace imported entries (deletes entries whose source is "import", then imports). Manual entries are never touched by an import.
-- **Locking**: "Require PIN for imported entries" locks every entry in that import. They wait as pending items until you enter your PIN ([7.7](#77-pending-locked-entries)).
-- **Report**, shown on the watch and in the Import status field, for example: "Imported 12 · 2 duplicates skipped · 1 error (entry 7 'GitHub': 9 digits not supported) · 1 skipped (mOTP)".
-- **Clean-up**: after a successful import, the watch clears the import fields and deletes the buffer.
-
-### 11.5 After an import
-
-Imported entries behave exactly like manual ones. You rename, regroup, move, lock, unlock, replace the secret of, or delete them with the Manage entry form, using the ID shown on the watch. The user guide also tells you to delete the exported JSON file from your phone and to keep it out of cloud-synced folders.
-
-## 12. Barcodes and QR codes
-
-### 12.1 Symbologies
-
-| Symbology | Accepted content | Options | Validation | Scope |
-| --- | --- | --- | --- | --- |
-| QR | Any text (UTF-8) | Error correction L/M/Q/H (default M) | Capacity for the chosen level; fits this screen | Versions 1 to 40, numeric/alphanumeric/byte modes with automatic segmentation, all 8 masks with standard penalty scoring. No Kanji, ECI or Micro QR |
-| Code 128 | ASCII 32 to 126 | Human-readable text | Characters; fits | Automatic switching between code sets A, B and C for the shortest result. GS1-128 (FNC1) deferred |
-| Code 39 | `0–9`, `A–Z`, space, `- . $ / + %` | Mod-43 check digit on or off; text | Characters; fits | Full ASCII mode deferred |
-| EAN-13 | 12 or 13 digits | Check digit: add or verify, fix, off; text | Digits only; check digit | EAN-2/EAN-5 add-ons deferred |
-| EAN-8 | 7 or 8 digits | As EAN-13 | As EAN-13 | |
-| UPC-A | 11 or 12 digits | As EAN-13 | As EAN-13 | |
-| UPC-E | 6, 7 or 8 digits (number system 0 or 1) | As EAN-13 | Must convert to UPC-A; check digit from the expanded form | |
-| ITF | An even number of digits | Mod-10 check digit on or off | Digits; an odd length needs a check digit or a leading 0 | Bearer bars (ITF-14 frames) deferred |
-| Codabar | `0–9 - $ : / . +` between start/stop characters `A–D` | | Characters. Missing start/stop characters are added as `A…A`, and the status says so | |
-| Data Matrix | Any text | | Capacity; fits | ECC 200, square sizes 10×10 to 144×144, ASCII (with digit pairs) and Base256 encoding. Rectangular sizes and C40/Text/X12/EDIFACT deferred |
-| PDF417 | Any text | Error correction (automatic by default) | Capacity; fits | Text, byte and numeric compaction; columns chosen to fit the screen. Compact, Micro and Macro PDF417 deferred |
-| Aztec | Any text | Error correction % (default 23 %) | Capacity; fits | Compact (1 to 4 layers) and full range (up to 32 layers). Structured append and runes deferred |
-
-### 12.2 Rendering
-
-- **Area**: 2D codes use the largest centred square (round screens) or rectangle. Linear codes and PDF417 use the widest horizontal band.
-- **Module size**: the largest whole number of pixels per module that fits, including the quiet zone (QR 4 modules, Data Matrix 1, PDF417 2, linear codes 10). The minimum is 2 px for 2D codes. Linear codes may use 1 px: an EAN-13 needs that on a 218 px screen, and the Forerunner 255S screenshot decodes at 1 px. Your phone and real scanners still need to confirm it (manual checklist).
-- **Colours**: always black modules on a white area that includes the quiet zone, whatever the theme.
-- **Human-readable text**: digits under linear codes when enabled and there's room.
-- **Brightness**: on AMOLED watches the app asks for full brightness while a code is shown, for up to about a minute. That's the burn-in protection limit; a tap boosts it again. On MIP watches the backlight is turned on.
-- **Caching**: the encoded matrix is stored encrypted (`x.<id>`) when the entry is saved, so opening it is instant. The drawn bitmap is cached for the session.
-- **Too big**: the watch says, for example, "Too large for this screen: needs 2 px per module, a 300 px square". The same warning goes to the status field when the entry is added.
-
-### 12.3 What fits (QR, byte mode, error correction M)
-
-| Screen | Largest centred square | At 3 px per module | At 2 px per module |
-| --- | --- | --- | --- |
-| 218 px round (Forerunner 255S) | 154 px | version 6, 106 bytes | version 13, 331 bytes |
-| 260 px round (fēnix 7, Forerunner 255) | 184 px | version 9, 180 bytes | version 16, 450 bytes |
-| 390 px round (vívoactive 5) | 276 px | version 16, 450 bytes | version 28 |
-| 454 px round (Forerunner 965) | 321 px | version 20, 666 bytes | version 33 |
-
-Linear: EAN-13 needs 113 modules, which means 1 px per module on a 218 px screen, 2 px on 260 px and 3 px on 390 px and larger. A 20-character Code 128 needs about 275 modules and doesn't fit 218 to 260 px round screens at all.
-
-### 12.4 How each symbology is proven scannable
-
-1. **Reference comparison**: Python reference encoders in Docker (zxing-cpp's writer, segno for QR with forced version and mask, and others) generate expected bar patterns and matrices for fixed parameters. The watch encoder must match them exactly wherever the output is deterministic: the EAN/UPC family, Code 39, ITF, Codabar, QR with forced version and mask, and Data Matrix in ASCII mode. Code 128 is the exception, because it has several equally short encodings. zxing-cpp 3 writes through zint, which breaks those ties differently from the zxing rules the watch uses. For Code 128 the module count must match (it did for 5,000 random strings), and the code must decode (check 2). For QR there are two deliberate differences from segno:
- - **Padding.** segno adds a whole zero byte when the bit stream already ends on a codeword boundary (always the case in byte mode). ISO/IEC 18004 7.4.10 adds no padding there, and the watch follows the standard. The exact references therefore use payloads that segno pads correctly.
- - **Segmentation.** segno encodes one mode per symbol, while the watch segments optimally. The exact references use single-mode payloads, and mixed content is covered by decoding.
-
- Data Matrix is compared with ppf.datamatrix in ASCII mode. ppf turns a pad codeword of exactly 254 into 0 (`(R + 130) % 254`), while ISO/IEC 16022 5.2.3 keeps 254, so the references avoid payloads whose padding hits that value. PDF417 is compared with pdf417gen at fixed columns and level. The watch mirrors pdf417gen's compaction rules, so its rows match exactly. Aztec is compared with aztec_code_generator for payloads of one character class (capitals, small letters or digits, with spaces). For those, the reference's optimal mode sequence and the watch's simpler encodation give the same bits, and the symbols match exactly, from compact to full range with reference grids. Mixed content is checked by decoding.
-2. **Decoding**: the simulator tests dump every encoder output. It's rendered at each device's module size and decoded with zxing-cpp, which reads all 12 symbologies. The decoded text must equal the input, for hundreds of seeded random payloads per symbology.
-3. **Screenshots**: simulator screenshots of the code screen on Forerunner 255S, Venu Sq 2 and Forerunner 965 must decode with zxing-cpp.
-4. **Real world** (manual checklist): your phone's camera, plus any real shop scanner you can try, indoors and outdoors, on MIP and AMOLED.
-
-A symbology counts as implemented only when checks 1 to 3 pass. Otherwise it's listed as deferred, with the reason, in this README and the changelog. **M6 result: all 12 are implemented**, with the comparison limits described above. The real-world check is your phone and scanners (manual checklist).
-
-## 13. UX design
-
-### 13.1 Screens
-
-- **List (home)**: entries in your order. With "Show groups" on, each row shows its group in front of the account ("Work · alice"). Separate group header lines were planned, but they need every entry's header at start-up, and M3 measured that as too slow for long lists.
- - Unlocked TOTP and HOTP entries show their current code and a small countdown.
- - Locked entries show a lock icon and their name (unless names are encrypted), and no code.
- - Codes show a barcode or QR icon and their name.
- - A banner appears when there are new settings results.
-- **OTP detail**: issuer and account, the code in large digits (grouped 3+3, 3+4 or 4+4), and a countdown ring along the bezel (a bar on rectangular screens) with the seconds left. In the last 5 seconds, the next code is shown. HOTP shows "Next" and the counter. Steam shows 5 characters. The entry ID is in small print at the bottom.
-- **Code detail**: the code centred on white, the name in small print if there's room, or a "Too large" message.
-- **PIN**: a keypad on touch screens; a digit selector on button-only watches. Digits show as dots. A wrong PIN vibrates (where available) and shows any waiting time.
-- **Create or change PIN**: enter 4 digits twice.
-- **Menu**: Lock now, Change PIN, Reset PIN (erases locked entries), Settings results, Entry IDs, Clock status, About (version, storage use, self-test result).
-- **Glance**: "OTP Wallet · 12 entries". If you turn it on, it shows the code of the first favourite TOTP or Steam entry without the PIN, with a progress bar, instead ([docs/SECURITY.md](docs/SECURITY.md#glance-code)).
-
-### 13.2 Flow
-
-```mermaid
-stateDiagram-v2
- [*] --> List
- List --> OtpDetail: select unlocked OTP
- List --> CodeDetail: select unlocked code
- List --> Pin: select locked entry, or pending items exist
- Pin --> OtpDetail: correct PIN, OTP entry
- Pin --> CodeDetail: correct PIN, code entry
- Pin --> List: back, or waiting after wrong PINs
- OtpDetail --> List: back, or auto-lock
- CodeDetail --> List: back, or auto-lock
- List --> Menu: menu
- Menu --> Pin: change or reset PIN
- Menu --> Results: settings results
- Results --> List: back
- Menu --> List: back
- List --> [*]: back, which locks everything
-```
-
-### 13.3 Wireframes (round screen)
-
-```text
- List OTP detail Code detail
- .-----------------. .-----------------. .-----------------.
- / OTP Wallet \ / GitHub \ / +-------------+ \
- | GitHub | | alice | | | ## # ### # # | |
- | alice 482 913 o | | | | | # ## # ### # | |
- | Google | | 482 913 | | | ### # # ## # | |
- | bob 705 228 o | | | | +-------------+ |
- | [lock] Bank | | next 119 004 | | Gym card |
- \ [bar] Gym card / \ 18 s / \ /
- '-----------------' '-----------------' '-----------------'
- (ring along the bezel = time left)
-
- PIN, touch PIN, buttons only Glance
- .-----------------. .-----------------. +----------------------+
- / Enter PIN \ / Enter PIN \ | OTP Wallet |
- | * * * o | | * * [5] o | | 12 entries |
- | 1 2 3 | | ^ | +----------------------+
- | 4 5 6 | | UP/DOWN digit | (opt-in: favourite code
- | 7 8 9 | | START next | and a progress bar)
- \ < 0 / \ BACK delete /
- '-----------------' '-----------------'
+```sh
+make image # Build the development container
+make sdk # Accept Garmin terms and download SDK/device files
+make key # Create a local developer key for packaging
+make build # Compile a debug build (default device: fr70)
+make test # Run tool tests and Monkey C unit tests
+make ci # Run the full build, test, packaging, and documentation checks
+make package # Build the signed Store package
```
-### 13.4 Input
-
-| Action | Touch | Buttons |
-| --- | --- | --- |
-| Scroll the list | Swipe up and down | UP and DOWN |
-| Open an entry | Tap it (START opens the focused row on watches that also have buttons) | START |
-| Back | Swipe right, or BACK | BACK |
-| Next HOTP code | Tap the code | START |
-| Menu | Long-press the list, or MENU | MENU (hold UP on most watches) |
-| Enter a PIN digit | Tap the keypad; the delete key deletes | UP/DOWN choose a digit, START adds it, BACK deletes |
-| Confirm the PIN | Automatic at the 4th digit | Automatic at the 4th digit |
-
-## 14. Target devices and API levels
-
-- **Manifest**: type `watch-app`, `minApiLevel` 5.0.0, no permissions, English.
-- **Products**: generated in M0 from the downloaded device definitions: every watch with API 5.0 or newer and at least 512 KB for watch apps. That includes fēnix 7, 8 and E; epix 2; Forerunner 165, 255, 265, 570, 955, 965 and 970; Venu 2, 3, 4, Sq 2 and X1; vívoactive 5 and 6; Instinct 3 AMOLED; Enduro 3; and related MARQ, quatix, tactix, D2, Descent and Approach models.
-- **Simulator test devices**: by default every test target runs on the Forerunner 70 only (the owner's watch), to keep runs and screenshot reviews short. `make <target> ALL=1` uses the full set below (`TEST_DEVICES`, `SPOT_DEVICES` and `WATCHDOG_DEVICES` in the Makefile), for example before a release:
- - Forerunner 255S: the weakest target (218 px round MIP, buttons only, 512 KB).
- - Venu Sq 2: rectangular AMOLED with touch.
- - Forerunner 70: your watch (390 px round AMOLED, touch and buttons).
- - Forerunner 965: 454 px round AMOLED with touch.
- - Spot checks: fēnix 7 (MIP with touch and buttons), Instinct 3 AMOLED (buttons only), Venu X1 (the largest rectangle).
-- **Memory**: watch app 512 to 768 KB; glance 64 KB. Budgets are in [section 18](#18-performance-budgets).
-
-## 15. Docker and sandbox
-
-### 15.1 Images
-
-| Image | Base | Contents | Network while building |
-| --- | --- | --- | --- |
-| `garmin-otp/dev` | `ubuntu:22.04` pinned by digest, apt from a dated `snapshot.ubuntu.com` mirror | OpenJDK 17; simulator libraries (`libwebkit2gtk-4.0-37`, `libusb-1.0-0`, `libsm6`, GTK); Xvfb, openbox, x11vnc, noVNC, xdotool, ImageMagick; a Python 3.10 virtual environment (pytest, hypothesis, zxing-cpp, segno, Pillow, all exact versions with hashes); `connect-iq-sdk-manager-cli` v0.8.4 (SHA-256 checked); make, openssl | yes |
-| `garmin-otp/docs` | Node LTS pinned by digest | markdownlint-cli2, mermaid-cli | yes |
-
-The Garmin SDK and device files are never baked into an image (Garmin's licence). They live in `.cache/`.
-
-### 15.2 How containers run
-
-- **Non-root**: as your user and group ID, with `--cap-drop ALL` and `--security-opt no-new-privileges`. No privileged mode, no host networking, and a read-only root filesystem with a tmpfs `/tmp` wherever the tools allow it.
-- **One mount**: only the project folder, at `/work`. `HOME` inside the container is `/work/.cache/home`, so the SDK, device files and login token stay inside the project.
-- **Network**: `--network none` for build, test, lint, package and docs runs. The network is used only to build images and in `make sdk` (Garmin downloads). `make sim` publishes noVNC on 127.0.0.1 only. That needs a bridge network for the port, but the app itself has no network permission.
-- **Removable build cache**: a dedicated buildx builder named `garmin-otp`, so its cache can be removed without touching other projects.
-- **Labels**: every container, image and the builder carry `project=garmin-otp`.
-
-### 15.3 First-time setup (your steps)
-
-1. **Docker access** (D5). Either:
- - `sudo usermod -aG docker cirakg`, then log out and back in. Membership in that group is equivalent to root on this machine.
- - Or rootless Docker, which needs the AUR package `docker-rootless-extras`.
-2. **`make image`** builds the images.
-3. **`make sdk`** (interactive, in a terminal):
- - Shows the link to Garmin's agreements and their version hash. You read them in your browser and type `accept`; the hash makes sure you accept exactly the text you read.
- - Asks for your Garmin email and password. They're passed only to the SDK manager CLI, which sends them to Garmin's sign-in page; neither is written to disk. The CLI's browser-based login can't be used, because it waits for a redirect to a random port inside the container, which your browser can't reach without host networking.
- - Downloads SDK 9.2.0 and the device files and simulator fonts for the manifest's products into `.cache/home/.Garmin/ConnectIQ`.
- - Deletes the login token at the end (`KEEP_LOGIN=1 make sdk` keeps it), so no Garmin credential stays in the project.
- - **Two-step verification**: password login doesn't support it. Fallback (not built yet): sign in through a browser running inside the container, shown at 127.0.0.1:6080, so the redirect stays inside the container.
-4. **`make key`** generates your developer key into `.cache/keys/` and prints backup instructions.
-5. **`make test`** and **`make sim`** confirm everything works.
-
-### 15.4 Make targets
-
-| Target | What it does | Network |
-| --- | --- | --- |
-| `make image` | Build the Docker images | yes |
-| `make sdk` | Licence, login, download the SDK and device files | yes (Garmin only) |
-| `make key` | Generate the developer key (never overwrites an existing one) | no |
-| `make build` | Debug build for `DEVICE` (default `fr70`) | no |
-| `make test` | Unit tests in the headless simulator on the 3 test devices | no |
-| `make sim` | Run the app in the simulator; view it at http://127.0.0.1:6080/vnc.html | localhost only |
-| `make screenshots` | Scripted UI flow with a screenshot of every screen on `TEST_DEVICES` and `SPOT_DEVICES` (barcode decoding from M6) | no |
-| `make screenshots-approve` | Save the current screens as the approved images, after you've reviewed them | no |
-| `make icons` | Regenerate the per-size launcher icons after the product list changes | no |
-| `make perf` | Performance measurements against the budgets | no |
-| `make memtest` | Memory after the first frame and in the glance, on `TEST_DEVICES` | no |
-| `make watchdog` | The largest code of every 2D symbology encoded, packed and drawn, and the worst PIN-unlock and phone-change work, at twice the steps per event, on `WATCHDOG_DEVICES` (Forerunner 70; with `ALL=1` also the 255S); fails on a watchdog trip or if the last code doesn't decode | no |
-| `make lint` | Strict type check (`-l 3`) with warnings as errors, project lint rules, settings XML schema check, Python lint for the tools | no |
-| `make package` | Signed `.iq` for the Store beta, plus per-device `.prg` files | no |
-| `make docs` | Markdown lint, link check, Mermaid render check | no (yes when building the docs image) |
-| `make ci` | lint, test, screenshots, perf, memtest, watchdog, package, docs | no |
-| `make products` | Check the manifest's product list against the downloaded device files | no |
-| `make lock` | Regenerate the Python and npm lock files (only releases older than the cutoff date) | yes |
-| `make vectors` | Regenerate the cross-checked crypto and OTP test vectors | no |
-| `make spike`, `make spike-sim` | Build the M1 settings spike; run it headless and screenshot it | no |
-| `make shell` | Shell in the dev container | no |
-| `make clean` | Delete build outputs | no |
-| `make nuke` | Delete everything this project created ([15.7](#157-uninstall-and-cleanup)) | no |
-
-### 15.5 Watching the simulator
-
-Run `make sim DEVICE=fr965`, then open http://127.0.0.1:6080/vnc.html in your browser. There's nothing to install. Forwarding your own X display into the container would also work, but it gives the container access to your desktop session, so it isn't the default.
-
-### 15.6 Developer key
-
-- **What**: an RSA-4096 key in PKCS#8 DER format, generated with openssl inside the container into `.cache/keys/developer_key.der` (plus `.pem`), mode 600. It's ignored by git and never baked into an image.
-- **Back it up**: copy `.cache/keys/` somewhere encrypted, such as a password-manager attachment or an encrypted USB drive.
-- **If you lose it**: Garmin rejects updates signed with a different key. You would have to create a new beta app (a new app ID), install it, and add every entry again. The watch data belongs to the old app ID and can't be exported.
-- `make nuke` deletes it, and asks you to confirm you have a backup first.
-
-### 15.7 Uninstall and cleanup
-
-1. **`make nuke`** asks for confirmation, then:
- - Stops and removes containers labelled `project=garmin-otp`.
- - Removes the images, and the buildx builder with its cache.
- - Deletes `.cache/` (SDK, device files, login token, developer key) and `build/`.
- - Removes the Ubuntu and Node base images too, unless another image on your machine still uses them.
-2. **Delete the project folder.**
-3. **If you joined the `docker` group** for this project: `sudo gpasswd -d cirakg docker`.
-4. **On the watch**: uninstall the app in the Connect IQ Store app, which deletes its data. Optionally delete the beta from your developer account.
-
-Nothing else is installed on your host: no packages, no `~/.Garmin`, no shell profile changes.
-
-## 16. Build plan
-
-The order follows your brief: crypto and OTP core, storage, UI, import, barcodes, settings, then polish. M1 (the settings spike) runs in parallel because it needs your phone and watch.
-
-**After every milestone**: `make ci` runs green in Docker, the docs are updated, `CHANGELOG.md` gets an entry, and you get a short status report with the test output.
+Build for another device with `make build DEVICE=<device-id>`. `make package` produces a signed `.iq` package and per-device `.prg` files under `build/`. To use phone settings, upload the package through Garmin's developer workflow and install the Store beta on a watch. See the [developer guide](docs/DEVELOPER_GUIDE.md) for the full workflow. The first `make image` and `make sdk` require network access; routine build and test targets use the prepared local container and SDK.
-### M0: Scaffolding (Phase 2)
+## Project documentation
-- **Deliverables**:
- - `docker/Dockerfile`, `Makefile` and the container scripts (SDK setup, headless simulator, test runner that parses the `monkeydo` output).
- - `.gitignore`, `CHANGELOG.md`.
- - `manifest.xml` with the generated product list, and `monkey.jungle` with debug, release and test configurations.
- - A minimal app that shows "OTP Wallet", plus one unit test.
-- **Tests**:
- - `make test` passes headless on the 3 test devices.
- - `make lint` passes with strict type checking.
- - `make package` produces an `.iq`.
- - `make nuke` leaves no project containers, images, builder or files behind; checked by listing before and after.
-- **Done when**: everything works from a clean checkout plus the first-time setup. The host has nothing besides Docker, and the network was used only for images and SDK downloads.
-- **Your part**: Docker access (D5); accepting the SDK licence and logging in (D6).
+- [User guide](docs/USER_GUIDE.md)
+- [Developer guide](docs/DEVELOPER_GUIDE.md)
+- [Testing and results](docs/TESTING.md)
+- [Security and storage format](docs/SECURITY.md)
+- [Performance](docs/PERFORMANCE.md)
+- [Project plan and design history](docs/PROJECT_PLAN.md)
-### M1: Settings spike (needs you; runs alongside M2)
+## Contributing
-- **Deliverables**:
- - A tiny spike app with its own beta app ID and a few test fields (long text, password, read-only status, switch, list, numeric). It shows on the watch what it received.
- - Test strings of increasing length, with markers.
- - `docs/SETTINGS_SPIKE.md`, recording the results.
-- **What we measure**:
- - Maximum length per field and in total, on your phone.
- - Which characters are accepted (`& < > \ " { } %`, non-ASCII).
- - Whether values the watch writes back (status, cleared fields) show up on the phone.
- - When `onSettingsChanged` fires.
- - How password fields behave.
-- **Done when**: the limits are recorded, and the number of import fields, the buffer size and the write-back assumptions in sections 10 and 11 are updated.
-- **Your part**: create a developer account, upload the spike as a beta, run the phone tests (about 30 minutes).
-
-### M2: Crypto and OTP core
-
-- **Deliverables**:
- - `crypto/`: hash wrappers, HMAC, SHA-512, PBKDF2, HKDF, AES-CBC with PKCS#7, encrypt-then-MAC records, constant-time comparison, zeroing, random, self-test.
- - `otp/`: Base32, HOTP, TOTP, Steam.
- - `util/clock` and `util/jobs`.
-- **Tests**: every vector listed in 17.2; the 2038 widening; worst-case inputs with no watchdog trip; speed of each primitive on the 3 devices; PBKDF2 calibration.
-- **Done when**: all vectors pass on the 3 devices, the numbers are in `docs/PERFORMANCE.md`, and AES-256 vs AES-128 is decided.
-
-### M3: Storage, keys and PIN logic
-
-- **Deliverables**: `storage/` (vault, TLV codec, key manager, session, lockout, migrations, quotas, pending queue).
-- **Tests**:
- - Round trips for every entry type.
- - Tamper detection: every byte of a record flipped in turn.
- - Wrong PIN; the lockout schedule with an injected clock, including after a restart; the erase option.
- - PIN change and reset.
- - Migration from a v0 test fixture to v1, and refusal of a newer schema.
- - A full store; zeroed buffers after use.
-- **Done when**: everything passes, the record size is measured, the entry cap is set, and the record format is in `docs/SECURITY.md`.
-
-### M4: UI core
-
-- **Deliverables**:
- - Views: list, OTP detail (ring, HOTP next, next-code preview), PIN entry (touch and buttons), menu, results screen, basic glance.
- - Layouts for round, rectangular and semi-octagon screens; theme and code-size options.
- - Debug-only fixture loader and performance overlay.
-- **Tests**:
- - Screenshot goldens of every screen on the 3 test devices, plus the spot checks.
- - Scripted input flows: open and close, correct PIN, wrong PIN, lockout display, auto-lock.
- - Start-up time and memory checkpoints.
-- **Done when**: you've reviewed the screenshots (attached to the status report), and start-up and memory are within budget in the simulator.
-
-### M5: Import
-
-- **Deliverables**:
- - `import/`: `otpauth://` parser, streaming JSON tokenizer, Aegis reader, import service with duplicate detection, partial imports, multi-part buffer and report.
- - Google migration import if time allows.
- - Wiring of the Import form.
-- **Tests**: parser edge cases, the Aegis fixtures and the fuzz corpus (17.2, 17.3), and an end-to-end import that's then checked in screenshots.
-- **Done when**: everything passes, and a 100-entry fake vault imports within the time and memory budgets.
-
-### M6: Barcodes
-
-Four parts:
-
-- **M6a**: the linear codes (EAN-13/8, UPC-A/E, Code 128, Code 39, ITF, Codabar), the renderer (fit, quiet zones, text, brightness) and validators.
-- **M6b**: QR.
-- **M6c**: Data Matrix and PDF417.
-- **M6d**: Aztec.
-
-Tests: the four checks in 12.4 for each symbology, plus validation tests (valid and invalid content, check digits added and fixed, capacity per error-correction level, fit warnings) and encode time and memory.
-
-Done when: each symbology is either marked implemented with every check green, or marked deferred with the reason.
-
-### M7: Settings
-
-- **Deliverables**:
- - `properties.xml` and `settings.xml` with every group.
- - Settings reader; inbox processor (Add OTP, Add code); manage processor; pending queue for locked entries.
- - Options applied live; status writer, results screen and catalog.
-- **Tests**:
- - Form parsing and validation for every OTP type and every symbology, valid and invalid.
- - Idempotency: the same form applied twice.
- - Manage actions on unlocked and locked entries.
- - End-to-end: a manually entered entry, set through a debug fixture that takes the same path as the phone, shows the right TOTP code or a barcode that decodes correctly, on screenshots.
-- **Done when**: everything passes, the XML validates against the SDK schema, and you've confirmed it on your phone and watch with the beta.
-- **Status (2026-10-20)**: everything passes in Docker and the XML validates; the phone and watch check with the beta is yours ([docs/TESTING.md](docs/TESTING.md#real-watch-checklist)).
-
-### M8: Polish, performance, release
-
-- **Deliverables**:
- - Tuning to the budgets; the memory check on Forerunner 255S; the glance favourite code (opt-in); AMOLED brightness handling.
- - Final `docs/` and the manual test checklist.
- - `make ci` green; signed `.iq` and per-device `.prg` files; `CHANGELOG` 1.0.0; the final list of known limitations.
-- **Done when**: `make ci` is green from a clean checkout, you've completed the on-watch checklist, and 1.0.0 is tagged.
-- **Status (2026-10-21)**: done in Docker as 0.8.0, the release candidate: `make ci` is green, also from a clean checkout; the signed `.iq` and the release `.prg` files are built by `make package`; the known limitations are in [docs/USER_GUIDE.md](docs/USER_GUIDE.md#known-limitations). Your on-watch checklist ([docs/TESTING.md](docs/TESTING.md#real-watch-checklist)) came before the 1.0.0 tag. **Status (2026-10-28)**: 1.0.0 tagged after the second real-watch round (0.9.0 on the Forerunner 70) and the settings spike follow-up.
-
-## 17. Test plan
-
-Tests are written alongside the code, never after.
-
-### 17.1 Layers
-
-| Layer | Tool | Runs in |
-| --- | --- | --- |
-| Unit tests (Monkey C) | Garmin's Run No Evil framework, `(:test)` functions | Headless simulator, 3 devices |
-| Test-vector generation, reference encoders, decoding, screenshot analysis | Python: pytest, hypothesis, zxing-cpp, segno, Pillow | Dev container |
-| UI and screenshot tests | Simulator, Xvfb, ImageMagick, xdotool | Dev container |
-| Performance and memory | Debug-build instrumentation in the simulator | Dev container |
-| Static checks | `monkeyc -l 3`, project lint, XML schema, ruff and mypy | Dev container |
-| Manual | The checklist in `docs/TESTING.md` | Your watch and phone |
-
-### 17.2 Unit test coverage
-
-- **crypto**:
- - FIPS 180-4 examples for SHA-1, SHA-256 and SHA-512: empty, "abc", the 448- and 896-bit messages, and one million "a" fed in chunks.
- - RFC 2202 (HMAC-SHA1); RFC 4231 (HMAC-SHA256 and HMAC-SHA512, all cases).
- - RFC 7914 §11 (PBKDF2-HMAC-SHA256); RFC 5869 (HKDF cases 1 to 3); NIST SP 800-38A (AES-128 and AES-256 CBC).
- - PKCS#7 edge cases; record tamper tests (every byte flipped); constant-time comparison; zeroing.
-- **otp**:
- - RFC 4226 (the 10 HOTP values) and RFC 6238 (6 times × 3 algorithms, 8 digits).
- - Truncation to 6, 7 and 8 digits; periods 15, 30, 60, 90 and odd values.
- - Rollover at each period boundary (k·p − 1, k·p, k·p + 1); the 2038 boundary (0x7FFFFFFF, and 0x80000000 read as unsigned); the year-2603 vector.
- - Steam, cross-checked against a Python reference.
- - Base32: the RFC 4648 §10 vectors, lowercase, whitespace, hyphens, padding, invalid characters, overlong input.
-- **storage**: the M3 list.
-- **import**:
- - `otpauth://`: percent-encoded labels, missing issuer, issuer mismatch, missing secret, invalid parameters, `hotp` without a counter, Steam variants, unknown parameters, `;` separators.
- - Aegis fixtures, all fake data: TOTP, HOTP, Steam, mOTP and Yandex; SHA1, SHA256 and SHA512; 6, 7 and 8 digits; periods 20, 30, 50 and 60; groups in database versions 1, 2 and 3; icons, notes and favourites; an encrypted vault; malformed and truncated (multi-part) JSON; huge strings, deep nesting, Unicode, duplicate keys.
- - Duplicates, partial imports, replace mode.
-- **settings**: every field of every form, valid and invalid, per OTP type and per symbology; idempotency; manage actions.
-- **codes**: section 12.4.
-- **util**: clock widening, offset and trusted-clock fallback; the job scheduler staying within a step budget; string helpers.
-
-### 17.3 Fuzz and property tests
-
-- **Inputs**: Python generates seeded random and mutated inputs (URIs, JSON documents, Base32 strings, settings forms, barcode contents), which are compiled into the test build.
-- **Monkey C assertions**: no unhandled exception; the result is either valid or a typed error with a message; the work stays within the watchdog step budget.
-- **Python side**: hypothesis checks the reference implementations used as test oracles.
-
-### 17.4 Simulator UI tests
-
-- **Setup**: the UI-test build (`uitest.jungle`, `source/util/TestMode.mc`) starts with fake entries, the PIN 1234 and a frozen clock. `tools/ui_flow.py` drives it through xdotool: it clicks the buttons from the device files, and taps and swipes on touch screens. It waits for each screen's `DIAG shown` line instead of sleeping.
-- **Screenshots** of every screen on the 4 test devices and the 3 spot-check devices are compared with approved goldens in `test/golden/<device>/`. At most 0.01 % of pixels may differ.
-- **Barcode screens** must decode with zxing-cpp.
-- **TOTP screens** are checked against the expected codes, from the debug build's view-model log and the golden image.
-
-### 17.5 End-to-end
-
-- **Manual entry**: a debug fixture sets the properties with `Properties.setValue` and calls the settings handler, the same path the phone triggers. The entry then goes through storage and into the list and detail screenshots, which must show the expected TOTP code or a barcode that decodes correctly.
-- **Imports**: the same path, with the import fixtures.
-
-### 17.6 Performance and memory
-
-See [section 18](#18-performance-budgets). `make perf` and `make memtest` fail the build when a budget is exceeded. `make watchdog` (M8) runs the largest input of every 2D encoder with twice the normal steps per event, so each step is proven to use at most half the watchdog budget, and draws the largest codes each screen fits. Since 0.9.0's review it also runs the work after a PIN (150 pending entries, 16 waiting "Require PIN" changes), reading the change queue and a 4 KB code replacement.
-
-### 17.7 Static checks
-
-- **Compiler**: `monkeyc -l 3` (strict) for every product, and any compiler warning fails the build.
-- **Project lint rules**:
- - No logging outside `(:debug)` code.
- - No secret-looking literals outside the fake-fixture folder.
- - Every string resource is used and defined.
- - No `TODO` in release code.
-- **Schema**: the settings and manifest XML are validated against the SDK's schemas.
-- **Python tools**: ruff and mypy.
-
-### 17.8 `make ci`
-
-Runs lint, test, screenshots, perf, memtest, watchdog, package and docs in that order, and stops at the first failure.
-
-### 17.9 Manual checklist on the real watch
-
-The full checklist will be in `docs/TESTING.md`. It covers what the simulator can't verify:
-
-- Installing the beta from the Store.
-- Adding and managing entries from your phone; the settings spike results.
-- Real clock and GPS sync; codes matching Aegis on your phone for the same accounts.
-- Crypto and start-up speed, using the performance overlay in a sideloaded debug build with fake data.
-- Button feel and touch targets.
-- Scanning with your phone and with real scanners, on MIP and AMOLED, indoors and outdoors.
-- Auto-lock and locking on exit; the system passcode, if your watch has one.
-- An app update keeps the data; uninstalling removes it; the glance.
-
-## 18. Performance budgets
-
-| Metric | Target on a watch | Simulator gate | How it's measured |
-| --- | --- | --- | --- |
-| Cold start to the first unlocked TOTP code drawn | ≤ 1.0 s on a mid-range watch (Forerunner 255 or 265) | ≤ 2× the M4 baseline | `System.getTimer()` from `AppBase.initialize` to the end of the first list draw. On the watch, the debug overlay |
-| TOTP with SHA-1 or SHA-256 | ≤ 30 ms | ≤ 2× baseline | Timer around the computation, averaged over 100 runs |
-| TOTP with SHA-512 (pure Monkey C) | ≤ 400 ms | ≤ 2× baseline | Same |
-| Countdown redraw | No frame over 100 ms; no allocations | Allocation counter in the debug build | Timer and `System.getSystemStats()` |
-| PIN unlock (key derivation) | 0.7 to 1.5 s, calibrated | Calibration logic tested | Timer |
-| Opening a code entry (cached matrix) | ≤ 300 ms | ≤ 2× baseline | Timer |
-| Encoding a QR version 10 without cache | ≤ 3 s, in chunks, with a progress indicator | ≤ 2× baseline | Timer |
-| Peak memory, app on Forerunner 255S | ≤ 70 % of 512 KB | Hard gate | `usedMemory` checkpoints and the simulator memory view |
-| Peak memory, glance | ≤ 70 % of 64 KB | Hard gate | Same |
-| Storage | ≤ 100 KB | Hard gate | Sum of record sizes |
-
-The simulator runs on your PC's CPU, so its timings don't represent a watch. In CI they only catch regressions against a baseline. Real numbers come from the debug overlay on your watch (manual checklist) and are reported in `docs/PERFORMANCE.md` with the method used.
-
-## 19. Risks and mitigations
-
-| Risk | Impact | Mitigation |
-| --- | --- | --- |
-| Real settings limits are smaller than assumed, or write-back doesn't work | Pastes don't fit; fields don't clear | Spike in M1, early; multi-part import; idempotent forms; results on the watch |
-| The iPhone Garmin app rejects `&` and `\` | Paste import fails on iPhone | Confirm in the spike; `;` accepted in URIs; Android workaround documented |
-| Plain-text secrets in settings (D2, D3) | Garmin and the phone see secrets | Documented in the threat model; fields cleared; advice to delete export files |
-| Array settings fixed later, or other settings bugs | | The inbox design doesn't depend on them |
-| Watchdog or slowness (SHA-512, PBKDF2, 2D encoders, big imports) | Crashes or lag | Resumable jobs; measured early (M2); caches |
-| Memory on 512 KB watches with every encoder loaded | Out of memory | Measured in M6 and M8; lean encoders; code size tracked per build |
-| The simulator's crypto differs from a real watch's (for example AES-256 support) | Wrong codes on the watch | Start-up self-test; manual device checks |
-| Ubuntu 22.04 support ends in April 2027; the simulator needs `libwebkit2gtk-4.0` | Image can't be rebuilt someday | Images pinned by digest; snapshot mirror; revisit when Garmin updates its requirements |
-| The third-party SDK-manager CLI breaks after a Garmin login change | Can't download device files | Fallback: Garmin's own SDK Manager inside the container, through noVNC |
-| Shop scanners can't read the watch screen (laser on AMOLED, MIP indoors) | Codes unusable at some tills | Brightness boost, quiet zones, minimum module size, manual scan tests; documented |
-| Large codes don't fit small screens | Some cards can't be shown | Validation and warnings at entry time |
-| Developer key lost | No updates possible | Backup instructions; `make nuke` confirmation |
-| Garmin changes the beta process | Can't install | Documented manual steps; checked in M1 |
-| Short PINs can be broken offline | Locked secrets exposed if storage is extracted | Documented in the user guide's known limitations; the PIN is fixed at 4 digits by the owner's choice (easier entry on the watch); Garmin's object-store encryption as a bonus |
-| The ~1 s PIN derivation feels slow | UX | Calibrated, with progress; one PIN per session (D1) |
-| Strict typing friction or compiler bugs | Slower development | SDK version pinned; issues documented |
-
-## 20. Documentation
-
-| Document | Content |
-| --- | --- |
-| `README.md` | This plan, kept up to date as the project's overview |
-| [`docs/FEASIBILITY.md`](docs/FEASIBILITY.md) | Phase 0 findings with sources |
-| `docs/YOUR_STEPS.md` | Your steps before 1.0.0: the settings spike, the on-watch tests and the results to send back |
-| `docs/STORE_LISTING.md`, `docs/store/` | Connect IQ Store listing: title, description, What's New, and the hero, cover and screen images (`python3 tools/store_assets.py`) |
-| `docs/USER_GUIDE.md` | Install the beta, import from Aegis, add codes, PIN and locking, daily use, moving to a new watch, troubleshooting |
-| `docs/DEVELOPER_GUIDE.md` | Repository layout, build, test and debug in Docker, adding a symbology or a device, coding conventions |
-| `docs/SECURITY.md` | Threat model, crypto choices and parameters, record formats, known limitations, how to report issues |
-| `docs/TESTING.md` | Test strategy, how to run everything, the manual checklist, results |
-| `docs/PERFORMANCE.md` | Budgets, methods and measured numbers |
-| `docs/SETTINGS_SPIKE.md` | The settings limits measured in M1 |
-| `CHANGELOG.md` | Every change, by version |
-
-## 21. Repository layout
-
-```text
-.
-├── README.md plan and overview
-├── CHANGELOG.md
-├── Makefile host entry point; every target runs in Docker
-├── manifest.xml
-├── monkey.jungle debug, release and test configurations (+ test, uitest, stress .jungle)
-├── docker/
-│ ├── Dockerfile garmin-otp/dev
-│ ├── docs.Dockerfile garmin-otp/docs
-│ └── scripts/ SDK setup, headless simulator, test runner, VNC
-├── icons.jungle generated: each product's launcher-icon folder
-├── source/
-│ ├── app/ crypto/ otp/ codes/ storage/ import/ settings/ ui/ util/
-├── test/ Monkey C (:test) modules, mirroring source/
-│ ├── data/ generated vectors and fake fixtures
-│ └── golden/ approved simulator screenshots
-├── resources/
-│ ├── settings/ properties.xml, settings.xml
-│ ├── strings/ drawables/ fonts/
-├── resources-icons/ generated: launcher icon at each size the products need
-├── tools/ Python: vector generation, reference encoders, decoding, screenshots, reports, lint
-├── docs/
-└── .cache/ build/ git-ignored: SDK, device files, keys, outputs
-```
+See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.
-## 22. Versioning
+## License
-Semantic versioning, with `CHANGELOG.md` in the "Keep a Changelog" style. Versions are 0.x during the milestones and 1.0.0 at M8. The app version is shown in the About screen and in the settings.
+Copyright (C) 2026 cirakg. This project is licensed under [GPL-3.0-only](LICENSE), without warranty. Generated barcode tables retain the upstream licenses listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). Garmin's SDK and device downloads are separate and are not included.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..9a7458a
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,15 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Vulnerability reporting and the limits of the app's security model.
+-->
+# Security policy
+
+Security fixes target the current release. The storage design, threat model and known limitations are documented in [docs/SECURITY.md](docs/SECURITY.md).
+
+## Reporting a vulnerability
+
+Send a private report to the maintainer at <cirakg@cirakg.xyz>. Include the affected version, watch model, impact, reproduction steps and a minimal example using fabricated data. Do not send real OTP seeds, PINs, authenticator exports, developer keys or watch storage dumps. Avoid posting exploit details publicly before a fix or coordinated disclosure. There is no guaranteed response time.
+
+## Known limits
+
+The four-digit PIN has only 10,000 possibilities. Lockout slows guesses on the watch, but cannot protect a copied storage dump from offline guessing. Unlocked entries use a device key stored alongside the app data. Secrets pasted into Garmin Connect settings pass through Garmin's infrastructure. Keep a secure backup in your primary authenticator; the app has no export or backup feature. Test keys in this repository are public demonstration data.
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
new file mode 100644
index 0000000..7eabb77
--- /dev/null
+++ b/THIRD_PARTY_NOTICES.md
@@ -0,0 +1,16 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Provenance and upstream licenses for generated barcode tables.
+-->
+# Third-party notices
+
+Original project code, documentation and artwork are licensed under [GPL-3.0-only](LICENSE). The following generated tables include material from upstream projects and retain their original notices and license terms. Include this file and the linked license texts with source and binary distributions, alongside the corresponding source for distributed GPL-covered binaries.
+
+| Material | Source used by the generator | License and notice |
+| --- | --- | --- |
+| QR error-correction blocks and alignment positions in `source/codes/QrTables.mc`, generated by `tools/gen_qr_tables.py` | [Segno](https://github.com/heuer/segno), version 1.6.6 | [BSD-3-Clause](LICENSES/segno.txt), Copyright 2016–2025 Lars Heuer |
+| PDF417 codeword, error-correction and text tables in `resources/pdf417/*.json`, generated by `tools/gen_pdf417_tables.py` | [pdf417gen](https://github.com/ihabunek/pdf417-py), version 0.8.1 | [MIT](LICENSES/pdf417gen.txt), Copyright 2016 Ivan Habunek |
+
+The versions above are pinned in `docker/requirements.txt`. Other Python, Node and system packages are development tools installed in the Docker images; their licenses remain with those packages. The SDK-manager CLI's Apache-2.0 license is retained in the development image by `docker/Dockerfile`.
+
+The Garmin Connect IQ SDK and device files are downloaded separately after accepting Garmin's terms. They are excluded from this repository and are not covered by the project's GPL license. Garmin and Connect IQ are trademarks of Garmin; this is an independent project.
diff --git a/docker/scripts/package.sh b/docker/scripts/package.sh
index 8d58e39..a0a22aa 100755
--- a/docker/scripts/package.sh
+++ b/docker/scripts/package.sh
@@ -42,4 +42,10 @@ for device in "${devices[@]}"; do
cp "${BUILD_DIR}/release/${device}.prg" "${out}/prg/${device}.prg"
done
+# Keep the upstream notices and project license with the binary artifacts.
+# The distributor must also provide the corresponding GPL-covered source.
+cp /work/LICENSE /work/THIRD_PARTY_NOTICES.md "${out}/"
+mkdir -p "${out}/LICENSES"
+cp /work/LICENSES/segno.txt /work/LICENSES/pdf417gen.txt "${out}/LICENSES/"
+
log "package ready: ${iq#/work/} ($(du -h "${iq}" | cut -f1)); release .prg files in build/package/prg/"
diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md
index d9b7c98..84476f9 100644
--- a/docs/DEVELOPER_GUIDE.md
+++ b/docs/DEVELOPER_GUIDE.md
@@ -4,14 +4,14 @@
-->
# Developer guide
-Everything runs in Docker through the `Makefile`; the host needs only Docker and make. This guide covers the daily workflow, how the tooling works underneath, and the conventions every change follows. The design and the build plan are in the [README](../README.md).
+Builds and tests run in Docker through the `Makefile`; the host needs only Docker and make. This guide covers the daily workflow, how the tooling works underneath, and the conventions every change follows. The original design and build plan are in [PROJECT_PLAN.md](PROJECT_PLAN.md). Optional host secret scanning and contribution guidelines are in [CONTRIBUTING.md](../CONTRIBUTING.md).
## First-time setup
1. Docker access: your user must be in the `docker` group (log out and in after joining, or run `newgrp docker` in a terminal).
2. `make image` builds the development image (about 1.1 GB; it needs the network).
3. `make sdk` is interactive: you accept Garmin's agreements, log in, and the SDK (9.2.0) plus the device files of all 72 products download into `.cache/home` (about 1.1 GB). The login token is deleted at the end.
-4. `make key` creates the developer key in `.cache/keys/`. Back that folder up; see [README 15.6](../README.md#156-developer-key).
+4. `make key` creates the developer key in `.cache/keys/`. Back that folder up; see [PROJECT_PLAN 15.6](PROJECT_PLAN.md#156-developer-key).
## Daily workflow
@@ -19,7 +19,7 @@ Everything runs in Docker through the `Makefile`; the host needs only Docker and
| --- | --- |
| Build for one device | `make build DEVICE=fr70` |
| Run the app and watch it in your browser | `make sim DEVICE=fr70`, then open http://127.0.0.1:6080/vnc.html |
-| Unit tests (Python tools and Monkey C, 4 devices) | `make test` |
+| Unit tests (Python tools and Monkey C, default device) | `make test` |
| All static checks | `make lint` |
| Screenshots against the approved images | `make screenshots` |
| Accept changed screens after reviewing build/screenshots/ | `make screenshots-approve` |
@@ -37,7 +37,7 @@ By default every target that takes a device list runs on the Forerunner 70 only
| Path | Content |
| --- | --- |
-| `source/` | The watch app (Monkey C), one folder per module (README section 5) |
+| `source/` | The watch app (Monkey C), one folder per module (PROJECT_PLAN section 5) |
| `test/` | Monkey C unit tests (`(:test)` functions), compiled only into test builds |
| `test/golden/` | Approved simulator screenshots: one folder per device, one image per screen of the UI flow |
| `resources/` | Strings, the default launcher icon, the option properties and, from M7, the settings form |
@@ -146,7 +146,7 @@ Debug builds print `DIAG first_frame startup_ms=... mem_used=... mem_total=...`
1. Declare each property in `resources/settings/properties.xml`. Reading an undeclared property crashes the app in a way that can't be caught.
2. Add the setting to `resources/settings/settings.xml` and its strings to `resources/strings/strings.xml`. `make lint` validates both against the SDK schema and checks that they agree (`tools/check_resources.py`).
3. Read the fields through a `Forms.Store` in a reader like `source/settings/OtpForm.mc`, so tests can use `Forms.MemoryStore`. Put every field in the form's hash content and its default in the form's `defaults()`; the test `settingsDefaultsMatchProperties` checks those against `properties.xml`.
- 4. Add a phase to `SettingsJob` in README 10.3 order. Heavy work (encoding, many records) goes into its own steps: one step is one timer event under the watchdog.
+ 4. Add a phase to `SettingsJob` in PROJECT_PLAN 10.3 order. Heavy work (encoding, many records) goes into its own steps: one step is one timer event under the watchdog.
- **On-watch numbers**: sideload a debug build (`make build DEVICE=fr70`, copy `build/debug/fr70.prg` to the watch's `GARMIN/APPS` folder over USB). Then open the menu and choose "Debug: benchmark". The watch installs the app and removes the `.prg` from the folder; to remove the app, use the watch's own app menu, then delete the `fr70.*` storage files in `GARMIN/APPS/DATA` and `GARMIN/APPS/SETTINGS` ([YOUR_STEPS.md](YOUR_STEPS.md#45-remove-the-debug-build)).
- **A barcode symbology**: add an encoder module under `source/codes/` and dispatch to it in `Codes.encode`. Then:
- Add reference rows in `tools/gen_code_vectors.py` (zxing-cpp's writer, or segno for QR) and run `make codes`.
diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md
index bf5d97b..981454d 100644
--- a/docs/PERFORMANCE.md
+++ b/docs/PERFORMANCE.md
@@ -4,7 +4,7 @@
-->
# Performance
-The targets are in [README section 18](../README.md#18-performance-budgets). This document records how each number is measured, what it does and doesn't tell you, and the results.
+The targets are in [PROJECT_PLAN section 18](PROJECT_PLAN.md#18-performance-budgets). This document records how each number is measured, what it does and doesn't tell you, and the results.
## How it is measured
@@ -22,7 +22,7 @@ The targets are in [README section 18](../README.md#18-performance-budgets). Thi
| `startup_ms` | 100 ms | M4 measured 1 to 18 ms; the margin absorbs simulator jitter |
| `first_frame_wall_ms` | 36000 ms | Regression guard only: about 2× M4's 16-18 s, which include loading the glance and opening the app from it |
| `mem_used_fraction` | 70 % | Hard limit, leaves room for spikes on the 512 KB watches |
-| `glance_mem_used_fraction` | 70 % | Hard limit on the glance's 64 KB (README 18), measured after its first draw |
+| `glance_mem_used_fraction` | 70 % | Hard limit on the glance's 64 KB (PROJECT_PLAN 18), measured after its first draw |
| Watchdog | Each job step at most half the per-event limit | `make watchdog`: the largest inputs at two steps per event (M8) |
## Results
@@ -35,7 +35,7 @@ The first numbers from the real watch, read from "Debug: benchmark" on the sidel
| --- | --- | --- | --- |
| TOTP, SHA-1 | 18 ms | under 1 ms | One code per timer event is far inside the watchdog |
| TOTP, SHA-512 | 732 ms | 17.5 ms | About 40 times SHA-1, as the simulator ratio predicted. A list with several SHA-512 entries fills in about one per second; the detail screen shows the code within a second |
-| PBKDF2 | 2,000 iterations per second | about 12,000 | Exactly the lower bound in README 18, so setting a PIN calibrates to the 2,000-iteration floor and unlocking takes about 1 s |
+| PBKDF2 | 2,000 iterations per second | about 12,000 | Exactly the lower bound in PROJECT_PLAN 18, so setting a PIN calibrates to the 2,000-iteration floor and unlocking takes about 1 s |
| Header reading | 0 entries in 1 ms | | The benchmark ran before the demo entries were added, so this says nothing yet |
| Self-test | 292 ms | | The crypto known-answer tests at start-up of the debug build |
| Memory at the benchmark | 179 of 763 KB (23 %) | 21 % at the first frame | Well under the 70 % budget |
@@ -81,7 +81,7 @@ Not covered yet: opening a 4 KB code (`toUtf8` in one event, about 100,000 bytec
| Version 10 | 312 ms, 105 steps | 139 ms, 106 steps |
| Version 26 | 1.4 s, 407 steps | 482 ms, 391 steps |
- Mask scoring now works on packed rows and columns (16 modules per word). The step counts didn't fall, because M8 found that M6's steps were too large for big codes (below) and sized every step by measurement. At 50 ms per tick, a version 10 QR needs at least 5.3 s on the 255S and 2.7 s on the 768 KB watches, plus the watch's compute, which only the on-watch benchmark can tell. README 18's 3 s target is met on the 768 KB watches at best; the cache makes every later opening instant.
+ Mask scoring now works on packed rows and columns (16 modules per word). The step counts didn't fall, because M8 found that M6's steps were too large for big codes (below) and sized every step by measurement. At 50 ms per tick, a version 10 QR needs at least 5.3 s on the 255S and 2.7 s on the 768 KB watches, plus the watch's compute, which only the on-watch benchmark can tell. PROJECT_PLAN 18's 3 s target is met on the 768 KB watches at best; the cache makes every later opening instant.
- **The demo cards in the simulated watches** (first encoding, `make screenshots`):
| Code | Forerunner 255S | 768 KB watches |
@@ -141,7 +141,7 @@ Not covered yet: opening a 4 KB code (`toUtf8` in one event, about 100,000 bytec
These times are mostly the 50 ms pauses between steps. QR is the slow one, because of its eight mask evaluations.
- Version 10 is over README 18's 3 s target. Most of the time is the pauses between steps, and mask scoring (eight masks, every module) is most of the steps. The cache makes every later opening instant. M8 can cut the steps, for example by scoring masks on packed rows. The demo card (version 6) encodes in 2.8 s in the simulated watches (`make screenshots` prints it).
+ Version 10 is over PROJECT_PLAN 18's 3 s target. Most of the time is the pauses between steps, and mask scoring (eight masks, every module) is most of the steps. The cache makes every later opening instant. M8 can cut the steps, for example by scoring masks on packed rows. The demo card (version 6) encodes in 2.8 s in the simulated watches (`make screenshots` prints it).
### M5, 2026-10-20 (version 0.5.0, import)
@@ -157,7 +157,7 @@ Not covered yet: opening a 4 KB code (`toUtf8` in one event, about 100,000 bytec
- **Watchdog.** The scripted UI flow showed it is enforced in normal simulator runs, at the device's limit per event (120,000 bytecodes on the Forerunner 255S, 240,000 on the others):
- **PBKDF2:** 200 byte-wise iterations in one step tripped it on the 255S. Rewritten to XOR 32-bit words, PBKDF2 takes 22 ms per 1,000 iterations in the simulator, down from 52 ms, and a step is now 100 iterations.
- **SHA-512:** one SHA-512 code per event fits on the 255S; two in one draw don't. The list and the detail screen compute codes in their own timer events, at most one SHA-512 code per event.
-- **On the watch.** The debug build's menu has "Debug: benchmark", which times TOTP with SHA-1 and SHA-512, PBKDF2 iterations per second, header reading and the self-test, one per timer event. Those are the numbers for README 18's watch targets; they come from your Forerunner 70 (real-watch checklist).
+- **On the watch.** The debug build's menu has "Debug: benchmark", which times TOTP with SHA-1 and SHA-512, PBKDF2 iterations per second, header reading and the self-test, one per timer event. Those are the numbers for PROJECT_PLAN 18's watch targets; they come from your Forerunner 70 (real-watch checklist).
### M3, 2026-10-19 (version 0.3.0, storage)
diff --git a/docs/PROJECT_PLAN.md b/docs/PROJECT_PLAN.md
new file mode 100644
index 0000000..3296b68
--- /dev/null
+++ b/docs/PROJECT_PLAN.md
@@ -0,0 +1,1076 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: Original design and build plan for the OTP Wallet Connect IQ app.
+-->
+# OTP Wallet for Connect IQ
+
+A Garmin Connect IQ watch app that shows one-time passwords (TOTP, HOTP, and optional Steam Guard codes) and stores barcodes and QR codes (loyalty cards, tickets) to show at a scanner. Everything lives encrypted on the watch. There is no cloud, no backend and no analytics, and the app requests no permissions, so it cannot use the network.
+
+> **Status: 1.0.0 released (2026-10-28)** after two real-watch rounds on the Forerunner 70 ([docs/TESTING.md](TESTING.md#results)). The Store listing text and images are in [docs/STORE_LISTING.md](STORE_LISTING.md). The history below is kept as written at M8.
+>
+> **M8 (polish, performance, release candidate) done in Docker as version 0.8.0.** The plan below was approved on 2026-10-18. M0 built the Docker toolchain and pipeline, M2 the crypto and OTP core, M3 the encrypted vault ([docs/SECURITY.md](SECURITY.md)), M4 the screens, M5 import (`otpauth://` and Aegis JSON via the phone settings), M6 all 12 barcode symbologies and M7 the phone settings forms. M8 added:
+>
+> - **The favourite code in the glance** (opt-in), and favourite marks in Manage entry.
+> - **Brightness** for barcodes: full brightness on AMOLED watches, the backlight on MIP watches, for about a minute.
+> - **Memory and speed:** a 100-entry import peaks at 45 % of the Forerunner 255S's memory instead of 56 %; QR mask scoring on packed rows; two job steps per tick on the 768 KB watches.
+> - **Watchdog safety:** every 2D encoder now handles the largest inputs the app accepts without being stopped by the watchdog, proven by `make watchdog` at twice the normal work per event.
+> - **Docs:** the [user guide](USER_GUIDE.md) with the [known limitations](USER_GUIDE.md#known-limitations).
+> - **Your next steps:** [docs/YOUR_STEPS.md](YOUR_STEPS.md), everything left for you before 1.0.0, in order.
+>
+> Waiting for you: the settings spike (`make spike`, [docs/SETTINGS_SPIKE.md](SETTINGS_SPIKE.md)) and the [real-watch checklist](TESTING.md#real-watch-checklist) with the 0.8.0 beta. The Phase 0 research, with sources, is in [docs/FEASIBILITY.md](FEASIBILITY.md); the daily workflow is in [docs/DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md).
+
+"OTP Wallet" is a working title.
+
+## Contents
+
+1. [Scope](#1-scope)
+2. [Decisions and open questions](#2-decisions-and-open-questions)
+3. [Feasibility findings](#3-feasibility-findings)
+4. [How the app reaches your watch](#4-how-the-app-reaches-your-watch)
+5. [Architecture](#5-architecture)
+6. [Data model](#6-data-model)
+7. [Security design](#7-security-design)
+8. [Threat model](#8-threat-model)
+9. [Time handling](#9-time-handling)
+10. [Settings design](#10-settings-design)
+11. [Import design](#11-import-design)
+12. [Barcodes and QR codes](#12-barcodes-and-qr-codes)
+13. [UX design](#13-ux-design)
+14. [Target devices and API levels](#14-target-devices-and-api-levels)
+15. [Docker and sandbox](#15-docker-and-sandbox)
+16. [Build plan](#16-build-plan)
+17. [Test plan](#17-test-plan)
+18. [Performance budgets](#18-performance-budgets)
+19. [Risks and mitigations](#19-risks-and-mitigations)
+20. [Documentation](#20-documentation)
+21. [Repository layout](#21-repository-layout)
+22. [Versioning](#22-versioning)
+
+## 1. Scope
+
+### What it will do
+
+- **One-time passwords**: TOTP (RFC 6238) and HOTP (RFC 4226) with SHA-1, SHA-256 or SHA-512, 6 to 8 digits, and any period from 1 to 300 seconds (default 30). Steam Guard codes are a separate, optional entry type.
+- **Barcodes and QR codes**: QR, Code 128, Code 39, EAN-13, EAN-8, UPC-A, UPC-E, ITF, Codabar, Data Matrix, PDF417 and Aztec. The watch stores the text and the symbology, then encodes and draws the code itself. A symbology ships only once it passes the checks in [section 12.4](#124-how-each-symbology-is-proven-scannable). Anything that doesn't pass is listed as deferred.
+- **Per-entry lock**: each entry has its own "Require PIN to display" setting. One app PIN unlocks a session.
+- **Encryption at rest** for every entry.
+- **Manual entry from your phone**: you add, edit, reorder and delete entries in the Connect IQ app settings, using an "add" form for each kind and a "manage" form.
+- **Import**: paste `otpauth://` URIs or an unencrypted Aegis JSON export into the app settings.
+- **Simple, fast watch UI**: a list of entries, a big code with a countdown ring, tap to advance an HOTP counter. It works with touch screens and with buttons only.
+
+### What it won't do
+
+| Not supported | Why |
+| --- | --- |
+| Unlocking with the watch's own PIN or passcode | Connect IQ has no API for it |
+| Encrypted Aegis exports | Decrypting one needs 32 MiB of RAM; watch apps get under 0.8 MB. You chose plain JSON import over an off-watch converter (decision D2) |
+| Aegis icons and notes, mOTP and Yandex entries | The watch doesn't decode images, notes may hold private text, and mOTP/Yandex weren't requested. They're skipped with a message |
+| Everyday use as a sideloaded app | Sideloaded apps get no phone settings, so you couldn't add entries. Sideloaded builds are for testing only |
+| Exporting or backing up entries from the watch | Deliberate: secrets only flow *into* the watch. Your Aegis export and your own records are the backup |
+| Scanning codes with the watch | There is no camera |
+| Very long linear codes and large 2D codes on small screens | They don't fit at a scannable size. The watch tells you instead of drawing an unreadable code |
+| Strong protection of locked entries if someone copies the data off the watch | A short PIN can be guessed offline ([section 8](#8-threat-model)) |
+| Any network feature | The app requests no permissions |
+
+## 2. Decisions and open questions
+
+Decided on 2026-10-18:
+
+| # | Decision | Choice |
+| --- | --- | --- |
+| D1 | PIN policy | Session unlock: enter the PIN once per session. Everything re-locks when you leave the app or after an inactivity timeout |
+| D2 | Import | Paste `otpauth://` URIs or an unencrypted Aegis JSON export into the settings; the watch parses them. No converter tool, no encrypted Aegis import |
+| D3 | Manual-entry layout | Inbox forms ("Add OTP", "Add code") plus a "Manage entry" form |
+| D4 | Targets | Watches with API 5.0 or newer and at least 512 KB for apps |
+
+Also settled on 2026-10-18, after this plan was approved:
+
+| # | Question | Answer |
+| --- | --- | --- |
+| D5 | Docker access | You joined the `docker` group |
+| Q1 | Test watch | Forerunner 70 (2026): API 6.0, 390×390 AMOLED, touch plus 5 buttons, 768 KB. It's one of the simulator test devices |
+| Q2 | Phone | Android, so the iPhone `&` and `\` problem doesn't affect you |
+
+Still open:
+
+| # | Question | Needed by |
+| --- | --- | --- |
+| D6 | Your Garmin account: accept the SDK licence and log in inside the container (`make sdk`); create a developer account for the beta | Phase 2 and M1 |
+
+## 3. Feasibility findings
+
+A summary. The details, evidence and sources are in [docs/FEASIBILITY.md](FEASIBILITY.md).
+
+| # | Question | Verdict | Consequence |
+| --- | --- | --- | --- |
+| 1 | Unlock entries with the Garmin device PIN | Not possible | App PIN with session unlock (D1). Garmin's system passcode, where a watch has one, shuts apps down when it locks and complements the app lock |
+| 2 | SHA-1/256/512, HMAC, PBKDF2, AES on the watch | Partially | Native: SHA-1, SHA-256, AES-ECB/CBC, HMAC-SHA256, random bytes. Built from their standards in Monkey C: HMAC-SHA1, SHA-512, PBKDF2, HKDF, encrypt-then-MAC |
+| 3 | Import and manual entry through app settings | Partially | Text fields only, no file upload. Settings only work for Store installs, so delivery is a private beta. Array settings are broken (CIQQA-4751), hence the inbox design (D3) |
+| 4 | Storage limits | Partially | Garmin's pages disagree. Design for at most 8 KB per value and about 100 KB in total |
+| 5 | Memory | Supported on the targets | 512 to 768 KB per app on API 5.x and 6.x watches |
+| 6 | Barcode rendering | Partially | All 12 symbologies can be drawn. Big codes don't fit small round screens. Scanner type matters |
+| 7 | App type | Supported | Device app with a glance |
+| 8 | Privacy of settings | Partially (undocumented) | Treat anything typed into settings as stored by Garmin. A consequence of D2 and D3 |
+| 9 | Encrypted Aegis export on the watch | Not possible | Out of scope (D2) |
+| 10 | TOTP after 2038 | Partially | The clock value is 32-bit. It's reinterpreted as unsigned, which is correct until 2106 |
+| 11 | Headless SDK, simulator and tests in Docker | Partially | Your Garmin login is needed once for device files. Ubuntu 22.04 base image |
+
+## 4. How the app reaches your watch
+
+Garmin's app settings only work for apps installed from the Connect IQ Store, so:
+
+1. `make package` builds a signed `.iq` file in Docker.
+2. You upload it on Garmin's developer website as a **beta app**, which only your account can see. Every new version is a new upload.
+3. You install it on your watch from the Connect IQ Store app.
+4. You manage entries in the Garmin Connect app on your phone: your watch, then Connect IQ apps, then OTP Wallet, then Settings.
+
+Development and automated tests use the simulator in Docker. For speed and rendering checks on a real watch, a sideloaded debug build contains fake fixture data and needs no settings.
+
+## 5. Architecture
+
+```mermaid
+flowchart TB
+ subgraph Phone["Phone: Garmin Connect app settings"]
+ F["Add OTP / Add code / Manage entry / Import / Options"]
+ end
+ subgraph Watch["Watch app (Monkey C)"]
+ APP["app: lifecycle, glance entry"]
+ UI["ui: list, OTP, code, PIN, menu, results, glance"]
+ SET["settings: form reader, inbox, manage, options, status"]
+ IMP["import: otpauth, Aegis JSON, JSON tokenizer"]
+ ST["storage: vault, records, keys, session, lockout, migrations"]
+ OTP["otp: Base32, HOTP, TOTP, Steam"]
+ COD["codes: encoders, Reed-Solomon, validators, renderer"]
+ CR["crypto: hashes, HMAC, SHA-512, PBKDF2, HKDF, AES-CBC + HMAC"]
+ UT["util: clock, jobs, bytes, errors, debug log"]
+ end
+ F -- "values, via Garmin" --> SET
+ APP --> UI
+ APP --> SET
+ SET --> IMP
+ SET --> ST
+ SET --> COD
+ IMP --> ST
+ UI --> ST
+ UI --> OTP
+ UI --> COD
+ ST --> CR
+ OTP --> CR
+ SET --> UT
+ ST --> UT
+ UI --> UT
+```
+
+| Module | Responsibility |
+| --- | --- |
+| `app/` | `AppBase`: lifecycle, initial view, glance view, `onSettingsChanged`, locking in `onStop` |
+| `crypto/` | Wrappers around the native hashes, HMAC (RFC 2104), SHA-512 (FIPS 180-4), PBKDF2 (RFC 8018), HKDF (RFC 5869), AES-CBC with PKCS#7 padding, encrypt-then-MAC records, constant-time comparison, zeroing, random bytes, start-up self-test |
+| `otp/` | Base32, HOTP, TOTP, Steam, parameter validation |
+| `codes/` | Bit-matrix and bar-pattern types, Reed–Solomon over GF(2^m) and GF(929), one encoder per symbology, validators, layout and renderer |
+| `storage/` | Vault (records and index), record codec, key manager (device key, locked master key, PIN wrapping), session, lockout, migrations, quotas, pending queue |
+| `import/` | `otpauth://` parser, streaming JSON tokenizer, Aegis reader (database versions 1 to 3), import service (duplicates, partial imports, multi-part buffer, report) |
+| `settings/` | Reads settings into typed forms; processes the inbox, manage, import and options forms; writes status fields and the catalog |
+| `ui/` | Views and input delegates; layouts for round, rectangular and semi-octagon screens |
+| `util/` | Clock (UTC, unsigned widening, offset, trusted-clock check), job scheduler, byte and string helpers, error types, debug-only log |
+
+Design rules:
+
+- **Thin UI.** Views draw view models. The logic lives in plain modules that tests can run without a screen.
+- **No network, no permissions.** Anyone can check this in `manifest.xml`.
+- **No watchdog trips.** Long work (key derivation, SHA-512 batches, 2D encoding, large imports) runs as resumable jobs on a timer, and the UI shows progress.
+- **No allocations in hot paths.** The 1 Hz countdown redraw reuses precomputed strings and geometry. Each code is computed once per period.
+- **Caches**: encoded barcode matrices (encrypted, in storage), drawn bitmaps per screen, and computed codes with the time they expire.
+- **Secrets are `ByteArray`s**, zeroed after use. The only secret-derived `String` is the short-lived code on screen.
+- **Debug and test code is excluded from release builds**: debug-only code (fixtures, performance overlay, logging) is annotated `(:debug)`, and tests are annotated `(:test)`.
+- **Code style**: heavy comments (purpose, inputs, outputs, errors, security notes), small functions, named constants, and every external input validated.
+
+## 6. Data model
+
+### 6.1 Entry fields
+
+| Field | Applies to | Where it's stored | Notes |
+| --- | --- | --- | --- |
+| `id` | all | header | 4 characters from `23456789ABCDEFGHJKMNPQRSTVWXYZ`, random and unique. Shown on the watch and used by the Manage form |
+| `kind` | all | header | OTP or code |
+| `type` | OTP | header | TOTP, HOTP or Steam |
+| `symbology` | code | header | One of the 12 |
+| `name` | all | header, or encrypted when "Encrypt names" is on | The issuer for OTP entries, the card name for codes |
+| `account` | OTP | same as `name` | The account label |
+| `group` | all | same as `name` | Optional |
+| `position` | all | index | Display order |
+| `locked` | all | header | "Require PIN to display" |
+| `source` | all | header | Manual or import. Used by "Replace imported entries" |
+| `favorite` | all | header | Comes from Aegis. The first favourite unlocked TOTP entry can appear in the glance |
+| `counter` | HOTP | header, covered by the record's MAC | Changes on every "next". Not secret |
+| `algorithm`, `digits`, `period` | OTP | encrypted | Not secret, but kept next to the secret so tampering is detected |
+| `secret` | OTP | encrypted | Raw bytes, decoded from Base32 on input |
+| `payload` | code | encrypted | UTF-8 text |
+| `options` | code | encrypted | Error-correction level, human-readable text, check-digit mode |
+| `created` | all | header | Used in reports |
+
+### 6.2 Storage layout (`Application.Storage`)
+
+| Key | Content | Protected by |
+| --- | --- | --- |
+| `m` | Metadata: schema version, entry index (the order of IDs), pending IDs | Not secret |
+| `dk` | Device key, 32 random bytes | Stored as is ([7.4](#74-what-stays-readable)) |
+| `w` | KDF salt, iteration count and the wrapped locked master key | The PIN key |
+| `lk` | Wrong-PIN count and delay | Not secret |
+| `e.<id>` | One entry record | Device key, or the locked master key for locked entries |
+| `x.<id>` | Cached barcode matrix for a code entry | The same key as its entry |
+| `p.<id>` | Pending locked entry, waiting for your PIN | Device key ([7.7](#77-pending-locked-entries)) |
+| `imp`, `imp.<n>` | Partial import text, kept between syncs, in chunks below 8 KB | Device key |
+| `q` | Phone changes to locked entries, waiting for your PIN ([7.9](#79-integrity-of-changes-coming-from-the-phone)) | Device key |
+| `qm.<n>` | The new secret or contents of a waiting "Replace", stored apart from `q` | Device key |
+| `res`, `fh`, `cs`, `st` | Latest settings results; keyed hashes of applied forms; signature of the last About update; date of the last self-test | Not secret |
+
+Limits: at most 8 KB per value and about 100 KB in total (the stricter of Garmin's two documented figures). M3 measured 239 bytes for a typical OTP record, so the entry cap stays at 150 (byte formats in [docs/SECURITY.md](SECURITY.md)). A full store produces a clear message, never a crash.
+
+### 6.3 Schema versions and migration
+
+- `m.schema` holds the data version. Each record also carries its own format version.
+- At start-up: an older schema runs migrations step by step (v1 to v2 to v3...). Each migration is a small, idempotent, unit-tested function. Before overwriting anything, a record is written under a temporary key and the index is switched to it, so a crash or power loss mid-migration loses nothing.
+- A newer schema (after a downgrade) is never modified. The app shows "Data from a newer version — update the app".
+- Unreadable or damaged records show as "Damaged entry K7F2 — re-add it" instead of crashing.
+- Garmin doesn't document what happens to storage on app updates. The manual test checklist verifies that an update keeps your data.
+
+## 7. Security design
+
+### 7.1 Keys
+
+```mermaid
+flowchart TD
+ PIN["PIN typed on the watch"] --> KDF["PBKDF2-HMAC-SHA256: random salt, iterations calibrated to about 1 s"]
+ KDF --> PK["PIN key: RAM only"]
+ PK -- "unwraps: AES-CBC + HMAC-SHA256" --> LMK["Locked master key: 32 random bytes, stored wrapped"]
+ DK["Device key: 32 random bytes, stored"]
+ LMK -- "HKDF" --> LK["encryption key + MAC key"]
+ DK -- "HKDF" --> DKK["encryption key + MAC key"]
+ LK --> LR["Locked entries"]
+ DKK --> UR["Unlocked entries, pending items, import buffer"]
+```
+
+### 7.2 PIN and key derivation
+
+- The PIN is exactly 4 digits, entered like the watch's own lock screen: the 4th digit submits it, with no OK key (changed after the real-watch test, 0.9.0). A PIN set by 0.8.0 or earlier (4 to 10 digits) still unlocks; the watch then asks for a new 4-digit PIN.
+- `PIN key = PBKDF2-HMAC-SHA256(PIN digits as ASCII, 16-byte random salt, N iterations, 32 bytes)`.
+- N is calibrated on your watch when you create or change the PIN, so one derivation takes about 1 s. It runs as a resumable job and is stored next to the salt. The lower and upper bounds for N are set from the M2 measurements.
+- The PIN key unwraps the locked master key. A correct PIN is exactly "the wrap's MAC verifies". No PIN hash is stored anywhere.
+- Changing the PIN only re-wraps the master key. Entries are not re-encrypted.
+
+### 7.3 Record encryption
+
+```text
+keys: Kenc, Kmac = HKDF-SHA256(master key, info = "OTPW enc v1" / "OTPW mac v1")
+record: v = 1 record format version
+ h = TLV(header fields) readable metadata (6.1)
+ kr = 0 | 1 0 = device key, 1 = locked master key
+ iv = 16 random bytes new on every write
+ ct = AES-CBC(Kenc, iv, PKCS7(TLV(sensitive fields)))
+ tag = HMAC-SHA256(Kmac, "OTPW-REC-1" || id || v || kr || h || iv || ct)
+```
+
+- **Encrypt-then-MAC.** The tag is checked in constant time *before* anything is decrypted, so tampering anywhere (header, IV, ciphertext) is detected.
+- **TLV** is a small deterministic byte encoding, so the MAC input is canonical.
+- **AES-256** if every target passes the M2 tests with it, otherwise AES-128. The PIN, not the cipher, is the weakest link either way.
+- **The PIN wrap** uses the same scheme, with its own keys derived from the PIN key and the label "OTPW-WRAP-1". The salt and iteration count are included in the MAC.
+- Only standard constructions are used: PBKDF2, HKDF, AES-CBC, HMAC, encrypt-then-MAC. Each is tested against official vectors (section 17.2).
+
+### 7.4 What stays readable
+
+- **The device key.** It must be on the watch so unlocked entries open without a PIN. Unlocked entries are therefore only obscured against someone reading raw storage. Garmin says app storage isn't accessible to users, and that Store-signed apps get an encrypted object store. The design doesn't rely on either.
+- **Record headers**: ID, kind, type or symbology, locked flag, HOTP counter, position, creation date, and names unless "Encrypt names" is on. This lets the list draw without decrypting anything.
+- **"Encrypt names"** (off by default) moves name, account and group into the ciphertext. Unlocked entries' names are then decrypted at start-up with the device key (the cost is measured), and locked entries show as "Locked entry K7F2" until you unlock.
+
+### 7.5 Session and auto-lock
+
+- **Unlocking**: PIN, then PIN key, then the master key is unwrapped and held in RAM.
+- **Locking happens** when you leave the app (`onStop`), after the inactivity timeout, or through "Lock now" in the menu. The timeout setting offers Immediately, 15 s, 30 s, 1 min (default), 2 min and 5 min. "Immediately" locks as soon as you leave a locked entry, which gives you the "PIN every time" behaviour whenever you want it.
+- **On lock**: the master key and derived keys are zeroed and dropped, cached codes and bitmaps of locked entries are discarded, and any open locked entry closes back to the list.
+
+### 7.6 Wrong PINs
+
+- The failure count and the next allowed attempt time are written to storage *before* each check (`Storage.setValue` writes to disk immediately) and reset on success. Restarting the app or the watch doesn't help an attacker.
+- Four free attempts. From the 5th failure, delays of 30 s, 1 min, 2 min, 4 min and so on, capped at 1 hour.
+- Delays are timed with the watch's trusted clock (`Time.CURRENT_TIME_RTC`, synced from GPS; the user can't change it). If that clock isn't synced, the delay counts app uptime instead and starts again in full after a restart: never shorter, sometimes longer.
+- The optional "Erase locked entries after 10 wrong PINs" setting is off by default. It deletes the wrapped master key, all locked entries and all pending items.
+- Forgotten PIN: "Reset PIN" erases the locked entries (they can't be recovered, by design) and sets a new PIN.
+
+### 7.7 Pending locked entries
+
+A locked entry can arrive from settings or an import while the watch is locked, or before any PIN exists. It can't be encrypted with the master key yet. It's stored under the device key as **pending**, which is no worse than the plain text that just came through settings, and hidden from the list. On the next open, the app asks for the PIN (or to create one), re-encrypts the entry under the master key, and zeroes and deletes the pending copy.
+
+### 7.8 Memory hygiene
+
+- Keys and decrypted secrets are `ByteArray`s, overwritten with zeros right after use. An unlocked session holds only the master key.
+- To show a code, the app decrypts the secret, computes the code and zeroes the secret. Only the short code string is kept, together with the time it expires.
+- **Limit**: Monkey C gives no guarantee that the VM didn't copy data (for example inside native crypto objects), and the system doesn't clear freed memory. This is best effort, and documented as such.
+- **No secrets in logs.** Release builds contain no logging. Debug logging goes through one function that refuses byte arrays and secret fields, and a lint rule enforces it.
+
+### 7.9 Integrity of changes coming from the phone
+
+Anyone who controls your phone or Garmin account can change the settings. So:
+
+- Actions on locked entries (edit, replace secret, unlock, set the counter, delete) wait in a queue until you confirm them with the PIN on the watch, even if the watch happens to be unlocked. The menu lists them under "Phone changes", where you confirm (the PIN is always asked) or reject them. "Require PIN" waits too while the watch is locked, because it needs the master key; it applies by itself at the next PIN entry, and what it did is listed under the menu's results.
+- A replacement secret or code contents is checked when it arrives: an invalid one is refused at once, and only a valid one waits.
+- Deleting or editing *unlocked* entries applies directly, and the watch shows what changed.
+- New entries always apply, and they're visible in the list.
+
+### 7.10 Start-up self-test
+
+On the first start after an install or update, and then once a day, the app runs known-answer tests: SHA-1, SHA-256 and SHA-512, HMAC, AES, and PBKDF2 with a low iteration count. If one fails, the app shows "Crypto self-test failed on this watch" and refuses to show codes rather than show wrong ones.
+
+## 8. Threat model
+
+**Assets**: OTP secrets, barcode contents, the PIN, entry names.
+
+| Scenario | Protected? | Notes |
+| --- | --- | --- |
+| Someone picks up your watch while the app is closed | Locked entries: yes. Unlocked entries: no, by design | Lock what matters. Turn on Garmin's system passcode if your watch has one |
+| Someone grabs the watch during an unlocked session | Until auto-lock (default: 60 s without input) | Choose a shorter timeout, or "Immediately" |
+| Lost or stolen watch, attacker uses the screen | Locked entries: yes | Lockout delays, optional erase |
+| Lost or stolen watch, attacker extracts the storage | Unlocked entries: no. Locked entries: only as strong as the PIN | 4 digits means 10,000 guesses. Each costs one ~1 s PBKDF2 run on the watch but microseconds on a PC, so a copied storage falls in seconds. The PIN protects against someone using the watch (delays after wrong PINs, optional erase), not against a storage copy. Garmin's object-store encryption for Store apps may stop the copy; it's undocumented, so it isn't relied on. The debug build's files were visible over USB on the Forerunner 70 (part 4 of docs/YOUR_STEPS.md) |
+| Garmin's servers and the Garmin Connect app | No, for anything typed or pasted into settings | A consequence of D2 and D3: secrets travel in the clear. The watch clears the fields after reading them, but Garmin may keep copies |
+| Malware on your phone (clipboard, accessibility services) | No | It can read what you paste. Clear the clipboard and delete the Aegis export after importing |
+| Someone controlling your phone or Garmin account changes settings | Partly | They can add, or delete unlocked, entries. Changes to locked entries need your PIN on the watch ([7.9](#79-integrity-of-changes-coming-from-the-phone)) |
+| Other Connect IQ apps on the watch | Yes | Garmin sandboxes each app's storage |
+| Network attackers | Yes | No network permission |
+| Shoulder surfing | Partly | A code is visible by nature. PIN digits show as dots |
+| Changing the watch clock to harvest future codes | Partly | Locked entries still need an unlocked session. The app warns when the clock disagrees with the trusted clock |
+| Compromised build tools | Partly | SDK from Garmin; versions pinned; images pinned by digest; Python packages pinned with hashes |
+| Out of scope | | Compromised watch firmware, RAM forensics on a running watch, hardware attacks, Garmin itself |
+
+## 9. Time handling
+
+- **Codes use UTC.** `Time.now()` gives UTC seconds, plus your offset setting (−120 to +120 s, default 0) for skew.
+- **2038**: Garmin stores time as a 32-bit signed integer. The app reinterprets negative values as unsigned 64-bit, which is correct until 2106. The OTP maths uses 64-bit time throughout, so the RFC 6238 test vector for the year 2603 passes.
+- **Trusted clock**: `Time.getCurrentTime({:currentTimeType => Time.CURRENT_TIME_RTC})` is synced from GPS and can't be changed by the user. If it isn't synced, a small clock icon warns that codes may be off. If it differs from the displayed time by more than 30 s, the app warns "Watch time differs from GPS time".
+- **If the time is wrong, codes are wrong.** The app can't fix that, but the warnings and the offset setting make it visible and correctable.
+- **HOTP**: the stored counter is the one whose code is on screen. "Next" saves counter + 1 first, then shows the new code, so a crash never reuses a code. If you also use the same HOTP account elsewhere, the counters drift apart.
+- **Refresh**: on the detail screen, a 1-second timer moves the countdown ring. The code is recomputed only at the period boundary, and in the last 5 seconds the next code is shown as a preview.
+
+## 10. Settings design
+
+### 10.1 Why inbox and manage forms
+
+Garmin's "array settings" (growable lists) are broken today. A failed save resets all of the app's settings (CIQQA-4751). Instead, there is one **Add OTP** form and one **Add code** form that the watch reads and then clears, plus one **Manage entry** form that acts on an entry by its ID. The watch is the database. The forms are how you send it requests.
+
+### 10.2 Groups and fields
+
+#### Add OTP
+
+| Property | Type | Field | Notes |
+| --- | --- | --- | --- |
+| `otpType` | number | list | TOTP, HOTP, Steam |
+| `otpIssuer` | string | alphaNumeric (max 40) | |
+| `otpAccount` | string | alphaNumeric (max 64) | |
+| `otpSecret` | string | password | Base32. Masked on screen only |
+| `otpAlgorithm` | number | list | SHA1, SHA256, SHA512 (ignored for Steam) |
+| `otpDigits` | number | list | 6, 7, 8 (Steam always shows 5 characters) |
+| `otpPeriod` | number | numeric (1 to 300) | TOTP. Steam is always 30 |
+| `otpCounter` | long | numeric (≥ 0) | HOTP starting counter |
+| `otpGroup` | string | alphaNumeric (max 32) | Optional |
+| `otpLocked` | boolean | switch | Require PIN to display |
+| `otpAdd` | boolean | switch | **Add this entry**: the watch only acts when this is on |
+| `otpStatus` | string | read-only | Result, written by the watch |
+
+#### Add code
+
+| Property | Type | Field | Notes |
+| --- | --- | --- | --- |
+| `codeName` | string | alphaNumeric (max 40) | |
+| `codeSymbology` | number | list | The 12 symbologies |
+| `codePayload` | string | alphaNumeric | The code's text or digits |
+| `codeEcLevel` | number | list | Low, Medium (default), Quartile, High. Mapped per symbology (QR L/M/Q/H; Aztec 10/23/36/50 %; PDF417 auto or levels 2/4/5/6) |
+| `codeCheckDigit` | number | list | Add or verify (default), Fix if wrong, Off |
+| `codeShowText` | boolean | switch | Human-readable text under linear codes |
+| `codeGroup` | string | alphaNumeric (max 32) | |
+| `codeLocked` | boolean | switch | Require PIN to display |
+| `codeAdd` | boolean | switch | **Add this entry** |
+| `codeStatus` | string | read-only | |
+
+#### Manage entry
+
+| Property | Type | Field | Notes |
+| --- | --- | --- | --- |
+| `mgId` | string | alphaNumeric | The entry ID shown on the watch, for example `K7F2` (case-insensitive) |
+| `mgAction` | number | list | Edit details, Move to position, Require PIN, Don't require PIN, Replace secret or contents, Set HOTP counter, Delete, Mark as favourite (glance), Remove favourite (M8) |
+| `mgName`, `mgAccount`, `mgGroup` | string | alphaNumeric | Blank means unchanged. `-` clears the group |
+| `mgPosition` | number | numeric (≥ 1) | |
+| `mgSecret` | string | password | New secret (Base32) or new code contents |
+| `mgCounter` | long | numeric | |
+| `mgApply` | boolean | switch | **Apply** |
+| `mgStatus` | string | read-only | |
+
+#### Import
+
+| Property | Type | Field | Notes |
+| --- | --- | --- | --- |
+| `impText1` to `impText8` | string | alphaNumeric | Joined in order. The settings spike (M1) measured 8000 characters per field and 64,000 in total arriving complete |
+| `impMode` | number | list | Append (default), Replace imported entries, Cancel pending import |
+| `impLocked` | boolean | switch | Require PIN for every entry in this import |
+| `impApply` | boolean | switch | **Import now** |
+| `impStatus` | string | read-only | |
+
+#### Options
+
+| Property | Field | Default |
+| --- | --- | --- |
+| `optAutoLock` | list: Immediately, 15 s, 30 s, 1 min, 2 min, 5 min | 1 min |
+| `optWipe` | switch: Erase locked entries after 10 wrong PINs | off |
+| `optTimeOffset` | numeric, −120 to 120 s | 0 |
+| `optCodeSize` | list: Normal, Large, Huge (code font) | Large |
+| `optTheme` | list: Dark, Light (barcodes are always black on white) | Dark |
+| `optGroups` | switch: Show groups | on |
+| `optEncryptNames` | switch | off |
+| `optGlanceCode` | switch: Show the favourite code in the glance | off |
+| `optBrightness` | switch: Full brightness while showing a barcode | on |
+
+**About** (all read-only, written by the watch): `catalog` (entry IDs and names, shortened to fit), `version`, `storageUse`.
+
+### 10.3 Processing rules
+
+1. **When**: from `onSettingsChanged` while the app runs, and at every app start. Processing runs *after* the first screen is drawn, so it never slows start-up.
+2. **Switches**: a form is processed only when its switch is on. Afterwards the watch turns the switch off and clears the form, secret fields first. After a problem only the secret field is cleared, so the other fields can be corrected and sent again.
+3. **Idempotency**: Garmin doesn't document whether the phone shows those cleared values ([FEASIBILITY 3e](FEASIBILITY.md#3e-writing-values-back-unknown)). So the watch also remembers a keyed hash of each of the last 32 applied forms and ignores a form it has already applied. Re-sending the same form does nothing; changing anything makes it a new request. Manage actions use stable IDs and absolute positions, so applying one twice can't hit a different entry.
+4. **Results** go to the form's status field and to a "Settings results" screen on the watch. That screen opens by itself after processing if the app is open, and is always available from the menu.
+5. **Status texts are short and specific**, for example:
+ - "Added K7F2 GitHub (alice)"
+ - "Secret: '1' is not a Base32 character (position 5)"
+ - "EAN-13: check digit should be 7, got 3. Choose 'Fix if wrong' to correct it"
+6. **Order**: Import, then Add OTP, then Add code, then Manage, then Options.
+
+### 10.4 Validation (OTP form)
+
+- **Secret**: Base32 (`A`–`Z`, `2`–`7`, any case). Spaces and hyphens are ignored, and `=` padding is optional. It must decode to 1 to 128 bytes, with a warning below 10 bytes (80 bits). Errors name the character and its position.
+- **Digits**: 6 to 8. Steam always uses 5 characters.
+- **Period**: 1 to 300 s. Steam always uses 30.
+- **Counter**: 0 to 2^63−1.
+- **Algorithm**: SHA1, SHA256 or SHA512. Steam always uses SHA1.
+- **Issuer and account**: at least one must be filled; no control characters.
+
+Code validation, per symbology, is in [section 12.1](#121-symbologies).
+
+### 10.5 XML excerpt
+
+The full files are in `resources/settings/` (written in M7). `make lint` validates them against the SDK's `resources.xsd` and checks that the form, the properties and the strings agree (`tools/check_resources.py`).
+
+```xml
+<!-- resources/settings/properties.xml (excerpt) -->
+<properties>
+ <property id="otpType" type="number">0</property>
+ <property id="otpIssuer" type="string"></property>
+ <property id="otpSecret" type="string"></property>
+ <property id="otpDigits" type="number">6</property>
+ <property id="otpPeriod" type="number">30</property>
+ <property id="otpCounter" type="long">0</property>
+ <property id="otpLocked" type="boolean">false</property>
+ <property id="otpAdd" type="boolean">false</property>
+ <property id="otpStatus" type="string"></property>
+</properties>
+```
+
+```xml
+<!-- resources/settings/settings.xml (excerpt) -->
+<settings>
+ <group id="addOtp" title="@Strings.GroupAddOtp" description="@Strings.GroupAddOtpHelp">
+ <setting propertyKey="@Properties.otpType" title="@Strings.OtpType">
+ <settingConfig type="list">
+ <listEntry value="0">@Strings.TypeTotp</listEntry>
+ <listEntry value="1">@Strings.TypeHotp</listEntry>
+ <listEntry value="2">@Strings.TypeSteam</listEntry>
+ </settingConfig>
+ </setting>
+ <setting propertyKey="@Properties.otpIssuer" title="@Strings.OtpIssuer">
+ <settingConfig type="alphaNumeric" maxLength="40" />
+ </setting>
+ <setting propertyKey="@Properties.otpSecret" title="@Strings.OtpSecret" prompt="@Strings.OtpSecretPrompt">
+ <settingConfig type="password" />
+ </setting>
+ <setting propertyKey="@Properties.otpDigits" title="@Strings.OtpDigits">
+ <settingConfig type="list">
+ <listEntry value="6">@Strings.Digits6</listEntry>
+ <listEntry value="7">@Strings.Digits7</listEntry>
+ <listEntry value="8">@Strings.Digits8</listEntry>
+ </settingConfig>
+ </setting>
+ <setting propertyKey="@Properties.otpPeriod" title="@Strings.OtpPeriod">
+ <settingConfig type="numeric" min="1" max="300" errorMessage="@Strings.OtpPeriodError" />
+ </setting>
+ <setting propertyKey="@Properties.otpLocked" title="@Strings.RequirePin">
+ <settingConfig type="boolean" />
+ </setting>
+ <setting propertyKey="@Properties.otpAdd" title="@Strings.AddThisEntry">
+ <settingConfig type="boolean" />
+ </setting>
+ <setting propertyKey="@Properties.otpStatus" title="@Strings.Status">
+ <settingConfig type="alphaNumeric" readonly="true" />
+ </setting>
+ </group>
+</settings>
+```
+
+## 11. Import design
+
+### 11.1 Accepted formats
+
+- **`otpauth://` URIs** for `totp`, `hotp` and `steam`. Paste one or many, separated by spaces or line breaks, or simply run together: a new URI starts at every `otpauth://`.
+- **Aegis unencrypted JSON export**: vault format version 1, database versions 1, 2 and 3. Detected by a leading `{`.
+- **Deferred: Google Authenticator** `otpauth-migration://offline?data=...` exports. This was the stretch goal. M5 ran out of the time set aside for it: the format is reverse-engineered (Base64 and protobuf), and its fixtures would have to be built from scratch. The importer recognises these links and explains that they aren't supported yet. Exporting from Aegis (which imports Google Authenticator) works today.
+
+### 11.2 Getting the text into settings
+
+- **Fields**: the eight import fields are joined in order. If the text is longer than one field accepts, continue in the next one.
+- **Several syncs**: if the JSON is still incomplete after joining, the watch keeps what it got (encrypted under the device key) and reports: "Waiting for more: received 3,214 characters. Paste the next part into the import fields and turn 'Import now' on again." "Cancel pending import" discards it. The buffer is capped at 32 KB and expires after 24 hours. M5 measured an Aegis export at about 190 bytes per entry compact, and 400 to 500 pretty-printed. Exports larger than the cap need to be split into several imports.
+- **Size reality**: pretty-printed Aegis JSON takes about 400 to 500 characters per entry without icons. Icons add several KB each and are the main reason a paste might not fit. An `otpauth://` URI takes about 80 to 200 characters.
+- **Spike results (M1, Android, 2026-10-21)**: one field carried 8000 characters and the eight together 64,000, so the 32 KB buffer, not Garmin Connect, limits one sync. `otpauth://` URIs with `&` save as they are. A test string mixing `< > \ " { } %`, accented letters and an emoji was refused by the phone. **Follow-up (round 2, 2026-10-27):** only the backslash `\` is refused; every other character group and a complete unencrypted Aegis export without backslashes saved, and a real Aegis export was imported on the Forerunner 70. Aegis JSON can be pasted when it has no `\` (Android Aegis writes `/` as `\/` in icons): replace `\/` with `/` or remove icons first (docs/SETTINGS_SPIKE.md).
+- **iPhone**: Garmin Connect on iOS reportedly rejects `&` (present in every URI with parameters) and `\` (used in JSON escapes). The spike ran on Android only. If it's confirmed on iOS, the parser also accepts `;` in place of `&`, and the user guide explains the workaround.
+
+### 11.3 Parsing rules
+
+#### `otpauth://` URIs
+
+- The label is percent-decoded and split on the first `:` (or `%3A`) into issuer and account, with surrounding spaces trimmed.
+- An `issuer` parameter wins over the label's issuer. A mismatch is noted in the report.
+- `secret` is required and follows the Base32 rules in 10.4.
+- Defaults: `algorithm` SHA1, `digits` 6, `period` 30. `counter` is required for `hotp`.
+- A `steam` type, or the `encoder=steam` parameter, means Steam.
+- Unknown parameters (`image`, `color` and so on) are ignored.
+- Errors name the URI's position, for example "URI 3: digits=9 is not supported (6–8)".
+
+#### Aegis JSON
+
+- **Encrypted exports are refused** with a message explaining how to export unencrypted. They're recognised by `header.slots` not being null, or `db` being a string.
+- **Types**: `totp`, `hotp` and `steam` are imported. `motp`, `yandex` and unknown types are skipped and reported.
+- **Fields used**: `name` (the account), `issuer`, `info.secret`, `info.algo`, `info.digits`, `info.period`, `info.counter`, `favorite` and groups (UUIDs resolved through the top-level `groups` list in database version 3, or the older `group` string). If an entry has several groups, the first is used.
+- **Ignored**: `icon`, `icon_mime` and `icon_hash` (skipped without loading them into memory) and `note` (may hold private text).
+- **Robust parsing**: the JSON tokenizer streams over the UTF-8 bytes, handles escapes and `\u` sequences, and enforces depth and length limits. It never builds large intermediate objects, and runs as a resumable job.
+
+### 11.4 Duplicates, partial imports and the report
+
+- **Duplicate**: the same type and the same secret as an existing entry. It's skipped and reported ("duplicate of K7F2").
+- **Same issuer and account, different secret**: imported as a new entry and flagged ("possible replacement for K7F2 — delete the old one with Manage entry").
+- **Partial imports**: every valid entry is stored, and invalid ones are listed with reasons. Entries are stored one at a time, so a crash or power loss leaves only whole entries.
+- **Modes**: Append, or Replace imported entries (deletes entries whose source is "import", then imports). Manual entries are never touched by an import.
+- **Locking**: "Require PIN for imported entries" locks every entry in that import. They wait as pending items until you enter your PIN ([7.7](#77-pending-locked-entries)).
+- **Report**, shown on the watch and in the Import status field, for example: "Imported 12 · 2 duplicates skipped · 1 error (entry 7 'GitHub': 9 digits not supported) · 1 skipped (mOTP)".
+- **Clean-up**: after a successful import, the watch clears the import fields and deletes the buffer.
+
+### 11.5 After an import
+
+Imported entries behave exactly like manual ones. You rename, regroup, move, lock, unlock, replace the secret of, or delete them with the Manage entry form, using the ID shown on the watch. The user guide also tells you to delete the exported JSON file from your phone and to keep it out of cloud-synced folders.
+
+## 12. Barcodes and QR codes
+
+### 12.1 Symbologies
+
+| Symbology | Accepted content | Options | Validation | Scope |
+| --- | --- | --- | --- | --- |
+| QR | Any text (UTF-8) | Error correction L/M/Q/H (default M) | Capacity for the chosen level; fits this screen | Versions 1 to 40, numeric/alphanumeric/byte modes with automatic segmentation, all 8 masks with standard penalty scoring. No Kanji, ECI or Micro QR |
+| Code 128 | ASCII 32 to 126 | Human-readable text | Characters; fits | Automatic switching between code sets A, B and C for the shortest result. GS1-128 (FNC1) deferred |
+| Code 39 | `0–9`, `A–Z`, space, `- . $ / + %` | Mod-43 check digit on or off; text | Characters; fits | Full ASCII mode deferred |
+| EAN-13 | 12 or 13 digits | Check digit: add or verify, fix, off; text | Digits only; check digit | EAN-2/EAN-5 add-ons deferred |
+| EAN-8 | 7 or 8 digits | As EAN-13 | As EAN-13 | |
+| UPC-A | 11 or 12 digits | As EAN-13 | As EAN-13 | |
+| UPC-E | 6, 7 or 8 digits (number system 0 or 1) | As EAN-13 | Must convert to UPC-A; check digit from the expanded form | |
+| ITF | An even number of digits | Mod-10 check digit on or off | Digits; an odd length needs a check digit or a leading 0 | Bearer bars (ITF-14 frames) deferred |
+| Codabar | `0–9 - $ : / . +` between start/stop characters `A–D` | | Characters. Missing start/stop characters are added as `A…A`, and the status says so | |
+| Data Matrix | Any text | | Capacity; fits | ECC 200, square sizes 10×10 to 144×144, ASCII (with digit pairs) and Base256 encoding. Rectangular sizes and C40/Text/X12/EDIFACT deferred |
+| PDF417 | Any text | Error correction (automatic by default) | Capacity; fits | Text, byte and numeric compaction; columns chosen to fit the screen. Compact, Micro and Macro PDF417 deferred |
+| Aztec | Any text | Error correction % (default 23 %) | Capacity; fits | Compact (1 to 4 layers) and full range (up to 32 layers). Structured append and runes deferred |
+
+### 12.2 Rendering
+
+- **Area**: 2D codes use the largest centred square (round screens) or rectangle. Linear codes and PDF417 use the widest horizontal band.
+- **Module size**: the largest whole number of pixels per module that fits, including the quiet zone (QR 4 modules, Data Matrix 1, PDF417 2, linear codes 10). The minimum is 2 px for 2D codes. Linear codes may use 1 px: an EAN-13 needs that on a 218 px screen, and the Forerunner 255S screenshot decodes at 1 px. Your phone and real scanners still need to confirm it (manual checklist).
+- **Colours**: always black modules on a white area that includes the quiet zone, whatever the theme.
+- **Human-readable text**: digits under linear codes when enabled and there's room.
+- **Brightness**: on AMOLED watches the app asks for full brightness while a code is shown, for up to about a minute. That's the burn-in protection limit; a tap boosts it again. On MIP watches the backlight is turned on.
+- **Caching**: the encoded matrix is stored encrypted (`x.<id>`) when the entry is saved, so opening it is instant. The drawn bitmap is cached for the session.
+- **Too big**: the watch says, for example, "Too large for this screen: needs 2 px per module, a 300 px square". The same warning goes to the status field when the entry is added.
+
+### 12.3 What fits (QR, byte mode, error correction M)
+
+| Screen | Largest centred square | At 3 px per module | At 2 px per module |
+| --- | --- | --- | --- |
+| 218 px round (Forerunner 255S) | 154 px | version 6, 106 bytes | version 13, 331 bytes |
+| 260 px round (fēnix 7, Forerunner 255) | 184 px | version 9, 180 bytes | version 16, 450 bytes |
+| 390 px round (vívoactive 5) | 276 px | version 16, 450 bytes | version 28 |
+| 454 px round (Forerunner 965) | 321 px | version 20, 666 bytes | version 33 |
+
+Linear: EAN-13 needs 113 modules, which means 1 px per module on a 218 px screen, 2 px on 260 px and 3 px on 390 px and larger. A 20-character Code 128 needs about 275 modules and doesn't fit 218 to 260 px round screens at all.
+
+### 12.4 How each symbology is proven scannable
+
+1. **Reference comparison**: Python reference encoders in Docker (zxing-cpp's writer, segno for QR with forced version and mask, and others) generate expected bar patterns and matrices for fixed parameters. The watch encoder must match them exactly wherever the output is deterministic: the EAN/UPC family, Code 39, ITF, Codabar, QR with forced version and mask, and Data Matrix in ASCII mode. Code 128 is the exception, because it has several equally short encodings. zxing-cpp 3 writes through zint, which breaks those ties differently from the zxing rules the watch uses. For Code 128 the module count must match (it did for 5,000 random strings), and the code must decode (check 2). For QR there are two deliberate differences from segno:
+ - **Padding.** segno adds a whole zero byte when the bit stream already ends on a codeword boundary (always the case in byte mode). ISO/IEC 18004 7.4.10 adds no padding there, and the watch follows the standard. The exact references therefore use payloads that segno pads correctly.
+ - **Segmentation.** segno encodes one mode per symbol, while the watch segments optimally. The exact references use single-mode payloads, and mixed content is covered by decoding.
+
+ Data Matrix is compared with ppf.datamatrix in ASCII mode. ppf turns a pad codeword of exactly 254 into 0 (`(R + 130) % 254`), while ISO/IEC 16022 5.2.3 keeps 254, so the references avoid payloads whose padding hits that value. PDF417 is compared with pdf417gen at fixed columns and level. The watch mirrors pdf417gen's compaction rules, so its rows match exactly. Aztec is compared with aztec_code_generator for payloads of one character class (capitals, small letters or digits, with spaces). For those, the reference's optimal mode sequence and the watch's simpler encodation give the same bits, and the symbols match exactly, from compact to full range with reference grids. Mixed content is checked by decoding.
+2. **Decoding**: the simulator tests dump every encoder output. It's rendered at each device's module size and decoded with zxing-cpp, which reads all 12 symbologies. The decoded text must equal the input, for hundreds of seeded random payloads per symbology.
+3. **Screenshots**: simulator screenshots of the code screen on Forerunner 255S, Venu Sq 2 and Forerunner 965 must decode with zxing-cpp.
+4. **Real world** (manual checklist): your phone's camera, plus any real shop scanner you can try, indoors and outdoors, on MIP and AMOLED.
+
+A symbology counts as implemented only when checks 1 to 3 pass. Otherwise it's listed as deferred, with the reason, in this README and the changelog. **M6 result: all 12 are implemented**, with the comparison limits described above. The real-world check is your phone and scanners (manual checklist).
+
+## 13. UX design
+
+### 13.1 Screens
+
+- **List (home)**: entries in your order. With "Show groups" on, each row shows its group in front of the account ("Work · alice"). Separate group header lines were planned, but they need every entry's header at start-up, and M3 measured that as too slow for long lists.
+ - Unlocked TOTP and HOTP entries show their current code and a small countdown.
+ - Locked entries show a lock icon and their name (unless names are encrypted), and no code.
+ - Codes show a barcode or QR icon and their name.
+ - A banner appears when there are new settings results.
+- **OTP detail**: issuer and account, the code in large digits (grouped 3+3, 3+4 or 4+4), and a countdown ring along the bezel (a bar on rectangular screens) with the seconds left. In the last 5 seconds, the next code is shown. HOTP shows "Next" and the counter. Steam shows 5 characters. The entry ID is in small print at the bottom.
+- **Code detail**: the code centred on white, the name in small print if there's room, or a "Too large" message.
+- **PIN**: a keypad on touch screens; a digit selector on button-only watches. Digits show as dots. A wrong PIN vibrates (where available) and shows any waiting time.
+- **Create or change PIN**: enter 4 digits twice.
+- **Menu**: Lock now, Change PIN, Reset PIN (erases locked entries), Settings results, Entry IDs, Clock status, About (version, storage use, self-test result).
+- **Glance**: "OTP Wallet · 12 entries". If you turn it on, it shows the code of the first favourite TOTP or Steam entry without the PIN, with a progress bar, instead ([docs/SECURITY.md](SECURITY.md#glance-code)).
+
+### 13.2 Flow
+
+```mermaid
+stateDiagram-v2
+ [*] --> List
+ List --> OtpDetail: select unlocked OTP
+ List --> CodeDetail: select unlocked code
+ List --> Pin: select locked entry, or pending items exist
+ Pin --> OtpDetail: correct PIN, OTP entry
+ Pin --> CodeDetail: correct PIN, code entry
+ Pin --> List: back, or waiting after wrong PINs
+ OtpDetail --> List: back, or auto-lock
+ CodeDetail --> List: back, or auto-lock
+ List --> Menu: menu
+ Menu --> Pin: change or reset PIN
+ Menu --> Results: settings results
+ Results --> List: back
+ Menu --> List: back
+ List --> [*]: back, which locks everything
+```
+
+### 13.3 Wireframes (round screen)
+
+```text
+ List OTP detail Code detail
+ .-----------------. .-----------------. .-----------------.
+ / OTP Wallet \ / GitHub \ / +-------------+ \
+ | GitHub | | alice | | | ## # ### # # | |
+ | alice 482 913 o | | | | | # ## # ### # | |
+ | Google | | 482 913 | | | ### # # ## # | |
+ | bob 705 228 o | | | | +-------------+ |
+ | [lock] Bank | | next 119 004 | | Gym card |
+ \ [bar] Gym card / \ 18 s / \ /
+ '-----------------' '-----------------' '-----------------'
+ (ring along the bezel = time left)
+
+ PIN, touch PIN, buttons only Glance
+ .-----------------. .-----------------. +----------------------+
+ / Enter PIN \ / Enter PIN \ | OTP Wallet |
+ | * * * o | | * * [5] o | | 12 entries |
+ | 1 2 3 | | ^ | +----------------------+
+ | 4 5 6 | | UP/DOWN digit | (opt-in: favourite code
+ | 7 8 9 | | START next | and a progress bar)
+ \ < 0 / \ BACK delete /
+ '-----------------' '-----------------'
+```
+
+### 13.4 Input
+
+| Action | Touch | Buttons |
+| --- | --- | --- |
+| Scroll the list | Swipe up and down | UP and DOWN |
+| Open an entry | Tap it (START opens the focused row on watches that also have buttons) | START |
+| Back | Swipe right, or BACK | BACK |
+| Next HOTP code | Tap the code | START |
+| Menu | Long-press the list, or MENU | MENU (hold UP on most watches) |
+| Enter a PIN digit | Tap the keypad; the delete key deletes | UP/DOWN choose a digit, START adds it, BACK deletes |
+| Confirm the PIN | Automatic at the 4th digit | Automatic at the 4th digit |
+
+## 14. Target devices and API levels
+
+- **Manifest**: type `watch-app`, `minApiLevel` 5.0.0, no permissions, English.
+- **Products**: generated in M0 from the downloaded device definitions: every watch with API 5.0 or newer and at least 512 KB for watch apps. That includes fēnix 7, 8 and E; epix 2; Forerunner 165, 255, 265, 570, 955, 965 and 970; Venu 2, 3, 4, Sq 2 and X1; vívoactive 5 and 6; Instinct 3 AMOLED; Enduro 3; and related MARQ, quatix, tactix, D2, Descent and Approach models.
+- **Simulator test devices**: by default every test target runs on the Forerunner 70 only (the owner's watch), to keep runs and screenshot reviews short. `make <target> ALL=1` uses the full set below (`TEST_DEVICES`, `SPOT_DEVICES` and `WATCHDOG_DEVICES` in the Makefile), for example before a release:
+ - Forerunner 255S: the weakest target (218 px round MIP, buttons only, 512 KB).
+ - Venu Sq 2: rectangular AMOLED with touch.
+ - Forerunner 70: your watch (390 px round AMOLED, touch and buttons).
+ - Forerunner 965: 454 px round AMOLED with touch.
+ - Spot checks: fēnix 7 (MIP with touch and buttons), Instinct 3 AMOLED (buttons only), Venu X1 (the largest rectangle).
+- **Memory**: watch app 512 to 768 KB; glance 64 KB. Budgets are in [section 18](#18-performance-budgets).
+
+## 15. Docker and sandbox
+
+### 15.1 Images
+
+| Image | Base | Contents | Network while building |
+| --- | --- | --- | --- |
+| `garmin-otp/dev` | `ubuntu:22.04` pinned by digest, apt from a dated `snapshot.ubuntu.com` mirror | OpenJDK 17; simulator libraries (`libwebkit2gtk-4.0-37`, `libusb-1.0-0`, `libsm6`, GTK); Xvfb, openbox, x11vnc, noVNC, xdotool, ImageMagick; a Python 3.10 virtual environment (pytest, hypothesis, zxing-cpp, segno, Pillow, all exact versions with hashes); `connect-iq-sdk-manager-cli` v0.8.4 (SHA-256 checked); make, openssl | yes |
+| `garmin-otp/docs` | Node LTS pinned by digest | markdownlint-cli2, mermaid-cli | yes |
+
+The Garmin SDK and device files are never baked into an image (Garmin's licence). They live in `.cache/`.
+
+### 15.2 How containers run
+
+- **Non-root**: as your user and group ID, with `--cap-drop ALL` and `--security-opt no-new-privileges`. No privileged mode, no host networking, and a read-only root filesystem with a tmpfs `/tmp` wherever the tools allow it.
+- **One mount**: only the project folder, at `/work`. `HOME` inside the container is `/work/.cache/home`, so the SDK, device files and login token stay inside the project.
+- **Network**: `--network none` for build, test, lint, package and docs runs. The network is used only to build images and in `make sdk` (Garmin downloads). `make sim` publishes noVNC on 127.0.0.1 only. That needs a bridge network for the port, but the app itself has no network permission.
+- **Removable build cache**: a dedicated buildx builder named `garmin-otp`, so its cache can be removed without touching other projects.
+- **Labels**: every container, image and the builder carry `project=garmin-otp`.
+
+### 15.3 First-time setup (your steps)
+
+1. **Docker access** (D5). Either:
+ - Add your account to the `docker` group using your operating system's user management tools, then log out and back in. Membership in that group is equivalent to root on the machine.
+ - Or rootless Docker, which needs the AUR package `docker-rootless-extras`.
+2. **`make image`** builds the images.
+3. **`make sdk`** (interactive, in a terminal):
+ - Shows the link to Garmin's agreements and their version hash. You read them in your browser and type `accept`; the hash makes sure you accept exactly the text you read.
+ - Asks for your Garmin email and password. They're passed only to the SDK manager CLI, which sends them to Garmin's sign-in page; neither is written to disk. The CLI's browser-based login can't be used, because it waits for a redirect to a random port inside the container, which your browser can't reach without host networking.
+ - Downloads SDK 9.2.0 and the device files and simulator fonts for the manifest's products into `.cache/home/.Garmin/ConnectIQ`.
+ - Deletes the login token at the end (`KEEP_LOGIN=1 make sdk` keeps it), so no Garmin credential stays in the project.
+ - **Two-step verification**: password login doesn't support it. Fallback (not built yet): sign in through a browser running inside the container, shown at 127.0.0.1:6080, so the redirect stays inside the container.
+4. **`make key`** generates your developer key into `.cache/keys/` and prints backup instructions.
+5. **`make test`** and **`make sim`** confirm everything works.
+
+### 15.4 Make targets
+
+| Target | What it does | Network |
+| --- | --- | --- |
+| `make image` | Build the Docker images | yes |
+| `make sdk` | Licence, login, download the SDK and device files | yes (Garmin only) |
+| `make key` | Generate the developer key (never overwrites an existing one) | no |
+| `make build` | Debug build for `DEVICE` (default `fr70`) | no |
+| `make test` | Unit tests in the headless simulator on the 3 test devices | no |
+| `make sim` | Run the app in the simulator; view it at http://127.0.0.1:6080/vnc.html | localhost only |
+| `make screenshots` | Scripted UI flow with a screenshot of every screen on `TEST_DEVICES` and `SPOT_DEVICES` (barcode decoding from M6) | no |
+| `make screenshots-approve` | Save the current screens as the approved images, after you've reviewed them | no |
+| `make icons` | Regenerate the per-size launcher icons after the product list changes | no |
+| `make perf` | Performance measurements against the budgets | no |
+| `make memtest` | Memory after the first frame and in the glance, on `TEST_DEVICES` | no |
+| `make watchdog` | The largest code of every 2D symbology encoded, packed and drawn, and the worst PIN-unlock and phone-change work, at twice the steps per event, on `WATCHDOG_DEVICES` (Forerunner 70; with `ALL=1` also the 255S); fails on a watchdog trip or if the last code doesn't decode | no |
+| `make lint` | Strict type check (`-l 3`) with warnings as errors, project lint rules, settings XML schema check, Python lint for the tools | no |
+| `make package` | Signed `.iq` for the Store beta, plus per-device `.prg` files | no |
+| `make docs` | Markdown lint, link check, Mermaid render check | no (yes when building the docs image) |
+| `make ci` | lint, test, screenshots, perf, memtest, watchdog, package, docs | no |
+| `make products` | Check the manifest's product list against the downloaded device files | no |
+| `make lock` | Regenerate the Python and npm lock files (only releases older than the cutoff date) | yes |
+| `make vectors` | Regenerate the cross-checked crypto and OTP test vectors | no |
+| `make spike`, `make spike-sim` | Build the M1 settings spike; run it headless and screenshot it | no |
+| `make shell` | Shell in the dev container | no |
+| `make clean` | Delete build outputs | no |
+| `make nuke` | Delete everything this project created ([15.7](#157-uninstall-and-cleanup)) | no |
+
+### 15.5 Watching the simulator
+
+Run `make sim DEVICE=fr965`, then open http://127.0.0.1:6080/vnc.html in your browser. There's nothing to install. Forwarding your own X display into the container would also work, but it gives the container access to your desktop session, so it isn't the default.
+
+### 15.6 Developer key
+
+- **What**: an RSA-4096 key in PKCS#8 DER format, generated with openssl inside the container into `.cache/keys/developer_key.der` (plus `.pem`), mode 600. It's ignored by git and never baked into an image.
+- **Back it up**: copy `.cache/keys/` somewhere encrypted, such as a password-manager attachment or an encrypted USB drive.
+- **If you lose it**: Garmin rejects updates signed with a different key. You would have to create a new beta app (a new app ID), install it, and add every entry again. The watch data belongs to the old app ID and can't be exported.
+- `make nuke` deletes it, and asks you to confirm you have a backup first.
+
+### 15.7 Uninstall and cleanup
+
+1. **`make nuke`** asks for confirmation, then:
+ - Stops and removes containers labelled `project=garmin-otp`.
+ - Removes the images, and the buildx builder with its cache.
+ - Deletes `.cache/` (SDK, device files, login token, developer key) and `build/`.
+ - Removes the Ubuntu and Node base images too, unless another image on your machine still uses them.
+2. **Delete the project folder.**
+3. **If you joined the `docker` group** for this project: remove your account from the group using your operating system's user management tools.
+4. **On the watch**: uninstall the app in the Connect IQ Store app, which deletes its data. Optionally delete the beta from your developer account.
+
+Nothing else is installed on your host: no packages, no `~/.Garmin`, no shell profile changes.
+
+## 16. Build plan
+
+The order follows your brief: crypto and OTP core, storage, UI, import, barcodes, settings, then polish. M1 (the settings spike) runs in parallel because it needs your phone and watch.
+
+**After every milestone**: `make ci` runs green in Docker, the docs are updated, `CHANGELOG.md` gets an entry, and you get a short status report with the test output.
+
+### M0: Scaffolding (Phase 2)
+
+- **Deliverables**:
+ - `docker/Dockerfile`, `Makefile` and the container scripts (SDK setup, headless simulator, test runner that parses the `monkeydo` output).
+ - `.gitignore`, `CHANGELOG.md`.
+ - `manifest.xml` with the generated product list, and `monkey.jungle` with debug, release and test configurations.
+ - A minimal app that shows "OTP Wallet", plus one unit test.
+- **Tests**:
+ - `make test` passes headless on the 3 test devices.
+ - `make lint` passes with strict type checking.
+ - `make package` produces an `.iq`.
+ - `make nuke` leaves no project containers, images, builder or files behind; checked by listing before and after.
+- **Done when**: everything works from a clean checkout plus the first-time setup. The host has nothing besides Docker, and the network was used only for images and SDK downloads.
+- **Your part**: Docker access (D5); accepting the SDK licence and logging in (D6).
+
+### M1: Settings spike (needs you; runs alongside M2)
+
+- **Deliverables**:
+ - A tiny spike app with its own beta app ID and a few test fields (long text, password, read-only status, switch, list, numeric). It shows on the watch what it received.
+ - Test strings of increasing length, with markers.
+ - `docs/SETTINGS_SPIKE.md`, recording the results.
+- **What we measure**:
+ - Maximum length per field and in total, on your phone.
+ - Which characters are accepted (`& < > \ " { } %`, non-ASCII).
+ - Whether values the watch writes back (status, cleared fields) show up on the phone.
+ - When `onSettingsChanged` fires.
+ - How password fields behave.
+- **Done when**: the limits are recorded, and the number of import fields, the buffer size and the write-back assumptions in sections 10 and 11 are updated.
+- **Your part**: create a developer account, upload the spike as a beta, run the phone tests (about 30 minutes).
+
+### M2: Crypto and OTP core
+
+- **Deliverables**:
+ - `crypto/`: hash wrappers, HMAC, SHA-512, PBKDF2, HKDF, AES-CBC with PKCS#7, encrypt-then-MAC records, constant-time comparison, zeroing, random, self-test.
+ - `otp/`: Base32, HOTP, TOTP, Steam.
+ - `util/clock` and `util/jobs`.
+- **Tests**: every vector listed in 17.2; the 2038 widening; worst-case inputs with no watchdog trip; speed of each primitive on the 3 devices; PBKDF2 calibration.
+- **Done when**: all vectors pass on the 3 devices, the numbers are in `docs/PERFORMANCE.md`, and AES-256 vs AES-128 is decided.
+
+### M3: Storage, keys and PIN logic
+
+- **Deliverables**: `storage/` (vault, TLV codec, key manager, session, lockout, migrations, quotas, pending queue).
+- **Tests**:
+ - Round trips for every entry type.
+ - Tamper detection: every byte of a record flipped in turn.
+ - Wrong PIN; the lockout schedule with an injected clock, including after a restart; the erase option.
+ - PIN change and reset.
+ - Migration from a v0 test fixture to v1, and refusal of a newer schema.
+ - A full store; zeroed buffers after use.
+- **Done when**: everything passes, the record size is measured, the entry cap is set, and the record format is in `docs/SECURITY.md`.
+
+### M4: UI core
+
+- **Deliverables**:
+ - Views: list, OTP detail (ring, HOTP next, next-code preview), PIN entry (touch and buttons), menu, results screen, basic glance.
+ - Layouts for round, rectangular and semi-octagon screens; theme and code-size options.
+ - Debug-only fixture loader and performance overlay.
+- **Tests**:
+ - Screenshot goldens of every screen on the 3 test devices, plus the spot checks.
+ - Scripted input flows: open and close, correct PIN, wrong PIN, lockout display, auto-lock.
+ - Start-up time and memory checkpoints.
+- **Done when**: you've reviewed the screenshots (attached to the status report), and start-up and memory are within budget in the simulator.
+
+### M5: Import
+
+- **Deliverables**:
+ - `import/`: `otpauth://` parser, streaming JSON tokenizer, Aegis reader, import service with duplicate detection, partial imports, multi-part buffer and report.
+ - Google migration import if time allows.
+ - Wiring of the Import form.
+- **Tests**: parser edge cases, the Aegis fixtures and the fuzz corpus (17.2, 17.3), and an end-to-end import that's then checked in screenshots.
+- **Done when**: everything passes, and a 100-entry fake vault imports within the time and memory budgets.
+
+### M6: Barcodes
+
+Four parts:
+
+- **M6a**: the linear codes (EAN-13/8, UPC-A/E, Code 128, Code 39, ITF, Codabar), the renderer (fit, quiet zones, text, brightness) and validators.
+- **M6b**: QR.
+- **M6c**: Data Matrix and PDF417.
+- **M6d**: Aztec.
+
+Tests: the four checks in 12.4 for each symbology, plus validation tests (valid and invalid content, check digits added and fixed, capacity per error-correction level, fit warnings) and encode time and memory.
+
+Done when: each symbology is either marked implemented with every check green, or marked deferred with the reason.
+
+### M7: Settings
+
+- **Deliverables**:
+ - `properties.xml` and `settings.xml` with every group.
+ - Settings reader; inbox processor (Add OTP, Add code); manage processor; pending queue for locked entries.
+ - Options applied live; status writer, results screen and catalog.
+- **Tests**:
+ - Form parsing and validation for every OTP type and every symbology, valid and invalid.
+ - Idempotency: the same form applied twice.
+ - Manage actions on unlocked and locked entries.
+ - End-to-end: a manually entered entry, set through a debug fixture that takes the same path as the phone, shows the right TOTP code or a barcode that decodes correctly, on screenshots.
+- **Done when**: everything passes, the XML validates against the SDK schema, and you've confirmed it on your phone and watch with the beta.
+- **Status (2026-10-20)**: everything passes in Docker and the XML validates; the phone and watch check with the beta is yours ([docs/TESTING.md](TESTING.md#real-watch-checklist)).
+
+### M8: Polish, performance, release
+
+- **Deliverables**:
+ - Tuning to the budgets; the memory check on Forerunner 255S; the glance favourite code (opt-in); AMOLED brightness handling.
+ - Final `docs/` and the manual test checklist.
+ - `make ci` green; signed `.iq` and per-device `.prg` files; `CHANGELOG` 1.0.0; the final list of known limitations.
+- **Done when**: `make ci` is green from a clean checkout, you've completed the on-watch checklist, and 1.0.0 is tagged.
+- **Status (2026-10-21)**: done in Docker as 0.8.0, the release candidate: `make ci` is green, also from a clean checkout; the signed `.iq` and the release `.prg` files are built by `make package`; the known limitations are in [docs/USER_GUIDE.md](USER_GUIDE.md#known-limitations). Your on-watch checklist ([docs/TESTING.md](TESTING.md#real-watch-checklist)) came before the 1.0.0 tag. **Status (2026-10-28)**: 1.0.0 tagged after the second real-watch round (0.9.0 on the Forerunner 70) and the settings spike follow-up.
+
+## 17. Test plan
+
+Tests are written alongside the code, never after.
+
+### 17.1 Layers
+
+| Layer | Tool | Runs in |
+| --- | --- | --- |
+| Unit tests (Monkey C) | Garmin's Run No Evil framework, `(:test)` functions | Headless simulator, 3 devices |
+| Test-vector generation, reference encoders, decoding, screenshot analysis | Python: pytest, hypothesis, zxing-cpp, segno, Pillow | Dev container |
+| UI and screenshot tests | Simulator, Xvfb, ImageMagick, xdotool | Dev container |
+| Performance and memory | Debug-build instrumentation in the simulator | Dev container |
+| Static checks | `monkeyc -l 3`, project lint, XML schema, ruff and mypy | Dev container |
+| Manual | The checklist in `docs/TESTING.md` | Your watch and phone |
+
+### 17.2 Unit test coverage
+
+- **crypto**:
+ - FIPS 180-4 examples for SHA-1, SHA-256 and SHA-512: empty, "abc", the 448- and 896-bit messages, and one million "a" fed in chunks.
+ - RFC 2202 (HMAC-SHA1); RFC 4231 (HMAC-SHA256 and HMAC-SHA512, all cases).
+ - RFC 7914 §11 (PBKDF2-HMAC-SHA256); RFC 5869 (HKDF cases 1 to 3); NIST SP 800-38A (AES-128 and AES-256 CBC).
+ - PKCS#7 edge cases; record tamper tests (every byte flipped); constant-time comparison; zeroing.
+- **otp**:
+ - RFC 4226 (the 10 HOTP values) and RFC 6238 (6 times × 3 algorithms, 8 digits).
+ - Truncation to 6, 7 and 8 digits; periods 15, 30, 60, 90 and odd values.
+ - Rollover at each period boundary (k·p − 1, k·p, k·p + 1); the 2038 boundary (0x7FFFFFFF, and 0x80000000 read as unsigned); the year-2603 vector.
+ - Steam, cross-checked against a Python reference.
+ - Base32: the RFC 4648 §10 vectors, lowercase, whitespace, hyphens, padding, invalid characters, overlong input.
+- **storage**: the M3 list.
+- **import**:
+ - `otpauth://`: percent-encoded labels, missing issuer, issuer mismatch, missing secret, invalid parameters, `hotp` without a counter, Steam variants, unknown parameters, `;` separators.
+ - Aegis fixtures, all fake data: TOTP, HOTP, Steam, mOTP and Yandex; SHA1, SHA256 and SHA512; 6, 7 and 8 digits; periods 20, 30, 50 and 60; groups in database versions 1, 2 and 3; icons, notes and favourites; an encrypted vault; malformed and truncated (multi-part) JSON; huge strings, deep nesting, Unicode, duplicate keys.
+ - Duplicates, partial imports, replace mode.
+- **settings**: every field of every form, valid and invalid, per OTP type and per symbology; idempotency; manage actions.
+- **codes**: section 12.4.
+- **util**: clock widening, offset and trusted-clock fallback; the job scheduler staying within a step budget; string helpers.
+
+### 17.3 Fuzz and property tests
+
+- **Inputs**: Python generates seeded random and mutated inputs (URIs, JSON documents, Base32 strings, settings forms, barcode contents), which are compiled into the test build.
+- **Monkey C assertions**: no unhandled exception; the result is either valid or a typed error with a message; the work stays within the watchdog step budget.
+- **Python side**: hypothesis checks the reference implementations used as test oracles.
+
+### 17.4 Simulator UI tests
+
+- **Setup**: the UI-test build (`uitest.jungle`, `source/util/TestMode.mc`) starts with fake entries, the PIN 1234 and a frozen clock. `tools/ui_flow.py` drives it through xdotool: it clicks the buttons from the device files, and taps and swipes on touch screens. It waits for each screen's `DIAG shown` line instead of sleeping.
+- **Screenshots** of every screen on the 4 test devices and the 3 spot-check devices are compared with approved goldens in `test/golden/<device>/`. At most 0.01 % of pixels may differ.
+- **Barcode screens** must decode with zxing-cpp.
+- **TOTP screens** are checked against the expected codes, from the debug build's view-model log and the golden image.
+
+### 17.5 End-to-end
+
+- **Manual entry**: a debug fixture sets the properties with `Properties.setValue` and calls the settings handler, the same path the phone triggers. The entry then goes through storage and into the list and detail screenshots, which must show the expected TOTP code or a barcode that decodes correctly.
+- **Imports**: the same path, with the import fixtures.
+
+### 17.6 Performance and memory
+
+See [section 18](#18-performance-budgets). `make perf` and `make memtest` fail the build when a budget is exceeded. `make watchdog` (M8) runs the largest input of every 2D encoder with twice the normal steps per event, so each step is proven to use at most half the watchdog budget, and draws the largest codes each screen fits. Since 0.9.0's review it also runs the work after a PIN (150 pending entries, 16 waiting "Require PIN" changes), reading the change queue and a 4 KB code replacement.
+
+### 17.7 Static checks
+
+- **Compiler**: `monkeyc -l 3` (strict) for every product, and any compiler warning fails the build.
+- **Project lint rules**:
+ - No logging outside `(:debug)` code.
+ - No secret-looking literals outside the fake-fixture folder.
+ - Every string resource is used and defined.
+ - No `TODO` in release code.
+- **Schema**: the settings and manifest XML are validated against the SDK's schemas.
+- **Python tools**: ruff and mypy.
+
+### 17.8 `make ci`
+
+Runs lint, test, screenshots, perf, memtest, watchdog, package and docs in that order, and stops at the first failure.
+
+### 17.9 Manual checklist on the real watch
+
+The full checklist will be in `docs/TESTING.md`. It covers what the simulator can't verify:
+
+- Installing the beta from the Store.
+- Adding and managing entries from your phone; the settings spike results.
+- Real clock and GPS sync; codes matching Aegis on your phone for the same accounts.
+- Crypto and start-up speed, using the performance overlay in a sideloaded debug build with fake data.
+- Button feel and touch targets.
+- Scanning with your phone and with real scanners, on MIP and AMOLED, indoors and outdoors.
+- Auto-lock and locking on exit; the system passcode, if your watch has one.
+- An app update keeps the data; uninstalling removes it; the glance.
+
+## 18. Performance budgets
+
+| Metric | Target on a watch | Simulator gate | How it's measured |
+| --- | --- | --- | --- |
+| Cold start to the first unlocked TOTP code drawn | ≤ 1.0 s on a mid-range watch (Forerunner 255 or 265) | ≤ 2× the M4 baseline | `System.getTimer()` from `AppBase.initialize` to the end of the first list draw. On the watch, the debug overlay |
+| TOTP with SHA-1 or SHA-256 | ≤ 30 ms | ≤ 2× baseline | Timer around the computation, averaged over 100 runs |
+| TOTP with SHA-512 (pure Monkey C) | ≤ 400 ms | ≤ 2× baseline | Same |
+| Countdown redraw | No frame over 100 ms; no allocations | Allocation counter in the debug build | Timer and `System.getSystemStats()` |
+| PIN unlock (key derivation) | 0.7 to 1.5 s, calibrated | Calibration logic tested | Timer |
+| Opening a code entry (cached matrix) | ≤ 300 ms | ≤ 2× baseline | Timer |
+| Encoding a QR version 10 without cache | ≤ 3 s, in chunks, with a progress indicator | ≤ 2× baseline | Timer |
+| Peak memory, app on Forerunner 255S | ≤ 70 % of 512 KB | Hard gate | `usedMemory` checkpoints and the simulator memory view |
+| Peak memory, glance | ≤ 70 % of 64 KB | Hard gate | Same |
+| Storage | ≤ 100 KB | Hard gate | Sum of record sizes |
+
+The simulator runs on your PC's CPU, so its timings don't represent a watch. In CI they only catch regressions against a baseline. Real numbers come from the debug overlay on your watch (manual checklist) and are reported in `docs/PERFORMANCE.md` with the method used.
+
+## 19. Risks and mitigations
+
+| Risk | Impact | Mitigation |
+| --- | --- | --- |
+| Real settings limits are smaller than assumed, or write-back doesn't work | Pastes don't fit; fields don't clear | Spike in M1, early; multi-part import; idempotent forms; results on the watch |
+| The iPhone Garmin app rejects `&` and `\` | Paste import fails on iPhone | Confirm in the spike; `;` accepted in URIs; Android workaround documented |
+| Plain-text secrets in settings (D2, D3) | Garmin and the phone see secrets | Documented in the threat model; fields cleared; advice to delete export files |
+| Array settings fixed later, or other settings bugs | | The inbox design doesn't depend on them |
+| Watchdog or slowness (SHA-512, PBKDF2, 2D encoders, big imports) | Crashes or lag | Resumable jobs; measured early (M2); caches |
+| Memory on 512 KB watches with every encoder loaded | Out of memory | Measured in M6 and M8; lean encoders; code size tracked per build |
+| The simulator's crypto differs from a real watch's (for example AES-256 support) | Wrong codes on the watch | Start-up self-test; manual device checks |
+| Ubuntu 22.04 support ends in April 2027; the simulator needs `libwebkit2gtk-4.0` | Image can't be rebuilt someday | Images pinned by digest; snapshot mirror; revisit when Garmin updates its requirements |
+| The third-party SDK-manager CLI breaks after a Garmin login change | Can't download device files | Fallback: Garmin's own SDK Manager inside the container, through noVNC |
+| Shop scanners can't read the watch screen (laser on AMOLED, MIP indoors) | Codes unusable at some tills | Brightness boost, quiet zones, minimum module size, manual scan tests; documented |
+| Large codes don't fit small screens | Some cards can't be shown | Validation and warnings at entry time |
+| Developer key lost | No updates possible | Backup instructions; `make nuke` confirmation |
+| Garmin changes the beta process | Can't install | Documented manual steps; checked in M1 |
+| Short PINs can be broken offline | Locked secrets exposed if storage is extracted | Documented in the user guide's known limitations; the PIN is fixed at 4 digits by the owner's choice (easier entry on the watch); Garmin's object-store encryption as a bonus |
+| The ~1 s PIN derivation feels slow | UX | Calibrated, with progress; one PIN per session (D1) |
+| Strict typing friction or compiler bugs | Slower development | SDK version pinned; issues documented |
+
+## 20. Documentation
+
+| Document | Content |
+| --- | --- |
+| `README.md` | Public-facing project overview and setup |
+| `docs/PROJECT_PLAN.md` | Original design plan and project history |
+| [`docs/FEASIBILITY.md`](FEASIBILITY.md) | Phase 0 findings with sources |
+| `docs/YOUR_STEPS.md` | Your steps before 1.0.0: the settings spike, the on-watch tests and the results to send back |
+| `docs/STORE_LISTING.md`, `docs/store/` | Connect IQ Store listing: title, description, What's New, and the hero, cover and screen images (`python3 tools/store_assets.py`) |
+| `docs/USER_GUIDE.md` | Install the beta, import from Aegis, add codes, PIN and locking, daily use, moving to a new watch, troubleshooting |
+| `docs/DEVELOPER_GUIDE.md` | Repository layout, build, test and debug in Docker, adding a symbology or a device, coding conventions |
+| `docs/SECURITY.md` | Threat model, crypto choices and parameters, record formats, known limitations, how to report issues |
+| `docs/TESTING.md` | Test strategy, how to run everything, the manual checklist, results |
+| `docs/PERFORMANCE.md` | Budgets, methods and measured numbers |
+| `docs/SETTINGS_SPIKE.md` | The settings limits measured in M1 |
+| `CHANGELOG.md` | Every change, by version |
+
+## 21. Repository layout
+
+```text
+.
+├── README.md public overview and setup
+├── docs/PROJECT_PLAN.md original plan and design history
+├── CHANGELOG.md
+├── Makefile host entry point; every target runs in Docker
+├── manifest.xml
+├── monkey.jungle debug, release and test configurations (+ test, uitest, stress .jungle)
+├── docker/
+│ ├── Dockerfile garmin-otp/dev
+│ ├── docs.Dockerfile garmin-otp/docs
+│ └── scripts/ SDK setup, headless simulator, test runner, VNC
+├── icons.jungle generated: each product's launcher-icon folder
+├── source/
+│ ├── app/ crypto/ otp/ codes/ storage/ import/ settings/ ui/ util/
+├── test/ Monkey C (:test) modules, mirroring source/
+│ ├── data/ generated vectors and fake fixtures
+│ └── golden/ approved simulator screenshots
+├── resources/
+│ ├── settings/ properties.xml, settings.xml
+│ ├── strings/ drawables/ fonts/
+├── resources-icons/ generated: launcher icon at each size the products need
+├── tools/ Python: vector generation, reference encoders, decoding, screenshots, reports, lint
+├── docs/
+└── .cache/ build/ git-ignored: SDK, device files, keys, outputs
+```
+
+## 22. Versioning
+
+Semantic versioning, with `CHANGELOG.md` in the "Keep a Changelog" style. Versions are 0.x during the milestones and 1.0.0 at M8. The app version is shown in the About screen and in the settings.
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index 3091fb4..10d52e5 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -4,7 +4,7 @@
-->
# Security and storage format
-This document records how the watch stores your data, byte for byte. The design rationale is in [README section 7](../README.md#7-security-design) and the threat model in [section 8](../README.md#8-threat-model). The code is in `source/storage/` and `source/crypto/`. Every claim below is covered by a unit test in `test/storage/` or `test/crypto/`.
+This document records how the watch stores your data, byte for byte. The design rationale is in [PROJECT_PLAN section 7](PROJECT_PLAN.md#7-security-design) and the threat model in [section 8](PROJECT_PLAN.md#8-threat-model). The code is in `source/storage/` and `source/crypto/`. The storage and crypto behavior is tested in `test/storage/` and `test/crypto/`. For private vulnerability reports, see the root [security policy](../SECURITY.md).
## Storage keys
@@ -28,7 +28,7 @@ Everything lives in the app's private `Application.Storage`:
| `cs` | string | Keyed hash of what the About fields and the glance code were last written from (IDs, pending IDs, "Encrypt names", the glance option, waiting changes, version); absent while names still need converting | `AboutScan` |
| `st` | array | Version and day of the last passed self-test | `AppState` |
-The PIN wrap and the lockout state have their own keys instead of living in `m` as README 6.2 first planned. Each wrong PIN rewrites the lockout state before the check, and a small separate value makes that write cheap and keeps it from risking the entry index.
+The PIN wrap and the lockout state have their own keys instead of living in `m` as PROJECT_PLAN 6.2 first planned. Each wrong PIN rewrites the lockout state before the check, and a small separate value makes that write cheap and keeps it from risking the entry index.
## Keys
@@ -97,7 +97,7 @@ Measured by `recordSizesAreSmall` with fake data (M3):
| Code entry (a 24-byte payload) | 208 bytes |
| Code entry with the largest allowed payload (4096 bytes) | 4,279 bytes |
-The per-value limit is 8 KB, and records over 8,000 bytes are refused. **The entry cap stays at 150.** That many OTP entries take about 36 KB, well under the conservative 100 KB total (README 6.2). Large barcode payloads can fill the store before the cap is reached. The vault then reports "storage full" cleanly, and nothing is half-written (`vaultHandlesAFullStore`). The field limits are 64 bytes for name and account, 32 bytes for the group, 128 bytes for an OTP secret and 4,096 bytes for a payload.
+The per-value limit is 8 KB, and records over 8,000 bytes are refused. **The entry cap stays at 150.** That many OTP entries take about 36 KB, well under the conservative 100 KB total (PROJECT_PLAN 6.2). Large barcode payloads can fill the store before the cap is reached. The vault then reports "storage full" cleanly, and nothing is half-written (`vaultHandlesAFullStore`). The field limits are 64 bytes for name and account, 32 bytes for the group, 128 bytes for an OTP secret and 4,096 bytes for a payload.
## PIN wrap, version 1
@@ -116,7 +116,7 @@ PIN key = PBKDF2-HMAC-SHA256(PIN as ASCII digits, salt, n, 32 bytes)
## Wrong PINs
-`Lockout` implements README 7.6:
+`Lockout` implements PROJECT_PLAN 7.6:
- **Attempts are counted before the check.** Each attempt is written to `lk` before the PIN is checked, so killing the app mid-check still costs an attempt. A success deletes `lk`.
- **Delays:** four free attempts, then 30 s after the 5th failure, doubling up to a 1 hour cap (`lockoutDelaySchedule`). An attempt during a delay is refused and not counted.
@@ -145,7 +145,7 @@ A locked entry that arrives while the session is locked, or before a PIN exists,
- **`m.schema` is the data version.** `Migration.run()` creates fresh metadata on first start and migrates older schemas step by step. It refuses newer schemas (VaultError NEWER_SCHEMA) and damaged metadata (DAMAGED) without changing them (`migrationRefusesNewerAndDamagedMetadata`).
- **Version 0 never shipped.** It is a fixture layout (`m = {"order": [...]}`) that tests the migration machinery (`migrationV0ToV1`).
-- **The 0-to-1 step changes only the metadata, in one write.** Future migrations that rewrite records must write each new record under a temporary key and switch the index before deleting the old one (README 6.3).
+- **The 0-to-1 step changes only the metadata, in one write.** Future migrations that rewrite records must write each new record under a temporary key and switch the index before deleting the old one (PROJECT_PLAN 6.3).
## Memory hygiene and its limits
@@ -155,7 +155,7 @@ A locked entry that arrives while the session is locked, or before a PIN exists,
- The native HMAC object's internal copy of its key.
- Any copy the VM makes.
- This is best effort, as README 7.8 says.
+ This is best effort, as PROJECT_PLAN 7.8 says.
- **Invalid UTF-8 is checked before decoding.** `StringUtil.convertEncodedString` stops the app with an uncatchable error on invalid UTF-8, so `Bytes.toUtf8` validates first, and damaged text can't crash the app.
## Test builds and fixtures
@@ -165,7 +165,7 @@ A locked entry that arrives while the session is locked, or before a PIN exists,
## Fingerprints (duplicate detection)
-Imports skip an entry that has "the same type and the same secret" as an existing one (README 11.4). That has to work for locked entries while the watch is locked. So every record carries an 8-byte keyed fingerprint in its readable header, tag 13:
+Imports skip an entry that has "the same type and the same secret" as an existing one (PROJECT_PLAN 11.4). That has to work for locked entries while the watch is locked. So every record carries an 8-byte keyed fingerprint in its readable header, tag 13:
```text
fingerprint = first 8 bytes of HMAC-SHA256(Kfp, "OTPW-FP-1" || kind || type || secret or payload)
@@ -184,7 +184,7 @@ Kfp = HKDF-SHA256(device key, info = "OTPW fp v1")
## Settings forms
-- **Form hashes.** The watch remembers the last 32 applied forms (README 10.3) as keyed hashes, because a form's content includes its secret:
+- **Form hashes.** The watch remembers the last 32 applied forms (PROJECT_PLAN 10.3) as keyed hashes, because a form's content includes its secret:
```text
hash = first 8 bytes of HMAC-SHA256(Kfp, "OTPW-FH-1" || 0x1F || form name || 0x1F || field 1 || 0x1F || ...)
@@ -192,7 +192,7 @@ Kfp = HKDF-SHA256(device key, info = "OTPW fp v1")
Kfp is the fingerprint key above. Without the device key a stored hash reveals nothing. With it, an attacker can only test guesses of a whole form, as with fingerprints. The fields go to the native HMAC one by one; the short ones (every secret field) are zeroed afterwards. An import form's text is too long to zero byte by byte within the watchdog limit; it stays in memory as the property's string anyway.
- **Secrets in the forms.** Add OTP's secret, Add code's contents and Manage entry's new secret are cleared first when the watch processes a form, also after a problem (the other fields stay so you can correct them). Messages never quote a secret.
-- **Changes from the phone to locked entries** (README 7.9). Edit, replace, set counter, "Don't require PIN", the favourite mark and delete of a locked entry are not applied when they arrive. Each one is sealed with the device key under `q`, which is no weaker than the settings text it came in. They are applied only after someone enters the PIN on the watch under "Phone changes", which always asks for the PIN, even while the session is unlocked. A replacement is validated when it arrives (Base32 decoded, or the code contents checked), and its material is sealed apart under `qm.<n>`, bound to its item number and entry ID. "Confirm all" and "Reject all" act only on the changes the menu showed (identified by their SealedBox IVs); any that arrive while the PIN is typed keep waiting (`queueStoresMaterialApart`, `queueChecksReplaceFirst`, `queueActsOnWhatWasShown`). Moving an entry is applied at once: the order isn't protected, and positions are absolute. Changes to unlocked entries apply at once and are listed on the results screen (`manageQueuesLockedEntryChanges`, `manageQueueIsSealedAndLimited`).
+- **Changes from the phone to locked entries** (PROJECT_PLAN 7.9). Edit, replace, set counter, "Don't require PIN", the favourite mark and delete of a locked entry are not applied when they arrive. Each one is sealed with the device key under `q`, which is no weaker than the settings text it came in. They are applied only after someone enters the PIN on the watch under "Phone changes", which always asks for the PIN, even while the session is unlocked. A replacement is validated when it arrives (Base32 decoded, or the code contents checked), and its material is sealed apart under `qm.<n>`, bound to its item number and entry ID. "Confirm all" and "Reject all" act only on the changes the menu showed (identified by their SealedBox IVs); any that arrive while the PIN is typed keep waiting (`queueStoresMaterialApart`, `queueChecksReplaceFirst`, `queueActsOnWhatWasShown`). Moving an entry is applied at once: the order isn't protected, and positions are absolute. Changes to unlocked entries apply at once and are listed on the results screen (`manageQueuesLockedEntryChanges`, `manageQueueIsSealedAndLimited`).
- **The About catalog** sends entry IDs and names to the phone's settings, where your entries came from in the first place. With "Encrypt names" on it sends only IDs and types.
- **"Encrypt names"** puts names and accounts into the encrypted part of each record. Turning it on or off re-seals every entry the watch can reach: unlocked ones at once, locked ones the next time the PIN is entered (`settingsAppliesEncryptNames`). The barcode cache is kept.
diff --git a/docs/SETTINGS_SPIKE.md b/docs/SETTINGS_SPIKE.md
index a6eb736..bd2aacb 100644
--- a/docs/SETTINGS_SPIKE.md
+++ b/docs/SETTINGS_SPIKE.md
@@ -4,7 +4,7 @@
-->
# Settings spike (M1)
-Garmin doesn't document how long a text setting may be, which characters the Garmin Connect app accepts, or whether values written by the watch show up on the phone. The inbox and import design (README sections 10 and 11) depends on those answers, so this milestone measures them with a small throwaway app, **Settings Spike**, on your Android phone and Forerunner 70.
+Garmin doesn't document how long a text setting may be, which characters the Garmin Connect app accepts, or whether values written by the watch show up on the phone. The inbox and import design (PROJECT_PLAN sections 10 and 11) depends on those answers, so this milestone measures them with a small throwaway app, **Settings Spike**, on your Android phone and Forerunner 70.
It takes about 30 minutes. Everything uses fake test strings; never paste a real secret here.
@@ -97,7 +97,7 @@ Results (2026-10-27, Android, the same phone and watch as above):
| C8 | a JSON fragment | Yes |
| aegis-json | a complete fake unencrypted Aegis export | Yes |
-The backslash alone blocks saving, which explains the `special` string. Aegis JSON can be pasted as long as it contains no `\`, so the import design (README 11) stays as it is: `otpauth://` links and unencrypted Aegis exports. The owner also imported a real Aegis export on the Forerunner 70 the same day.
+The backslash alone blocks saving, which explains the `special` string. Aegis JSON can be pasted as long as it contains no `\`, so the import design (PROJECT_PLAN 11) stays as it is: `otpauth://` links and unencrypted Aegis exports. The owner also imported a real Aegis export on the Forerunner 70 the same day.
Aegis on Android writes JSON with Android's `org.json`, which escapes `/` as `\/`. A `/` appears in entry icons (base64 data and `"image\/png"`) and sometimes in a name or issuer; a `"` in a name is written as `\"`. Such an export is refused by the phone. Workarounds, in the user guide: replace every `\/` with `/` in a text editor (the JSON stays valid), remove the entry icons in Aegis before exporting, or paste `otpauth://` links instead. Test C3 is accepted as a known limitation; no fix is planned.
@@ -105,7 +105,7 @@ This decides whether Aegis JSON can be pasted into the import fields at all (it
### What the results change
-- **Import field size (A, B).** The import design assumed fields of a few hundred characters. One field takes at least 8000 characters and the eight together at least 64,000, so Garmin Connect is not the limit: the watch's 32 KB import buffer is (README 11.2). About 160 compact Aegis entries, or 60 to 80 pretty-printed ones, fit in one sync. The eight `impText` fields and multi-part import stay as they are.
+- **Import field size (A, B).** The import design assumed fields of a few hundred characters. One field takes at least 8000 characters and the eight together at least 64,000, so Garmin Connect is not the limit: the watch's 32 KB import buffer is (PROJECT_PLAN 11.2). About 160 compact Aegis entries, or 60 to 80 pretty-printed ones, fit in one sync. The eight `impText` fields and multi-part import stay as they are.
- **`otpauth://` URIs (C).** They paste as they are, so the parser doesn't need the `;` fallback for `&`.
- **Write-back (D).** Status lines written by the watch (the forms' **Result** fields) and cleared secret fields show on the phone after reopening the settings.
- **Processing (E, G).** Changes arrive within about a second while the app is open, and at the next start when it is closed. The app already processes forms on both paths.
@@ -113,9 +113,9 @@ This decides whether Aegis JSON can be pasted into the import fields at all (it
## How the results feed the design
-- **A and B** set the number and size of the import fields (`impText1` to `impTextN`, README 10.2) and the multi-part import buffer (README 11.2). For example, if one field holds 4000 characters but the total stops at 8000, import uses two or three fields per sync and multi-part import for larger Aegis files.
-- **C** decides whether `otpauth://` URIs can be pasted as they are on your phone and whether the parser needs the `;` fallback (README 11.2).
-- **D** decides whether status messages and cleared secret fields are visible on the phone. The design works either way (forms are idempotent and results also show on the watch, README 10.3); this tells us which one you'll experience.
+- **A and B** set the number and size of the import fields (`impText1` to `impTextN`, PROJECT_PLAN 10.2) and the multi-part import buffer (PROJECT_PLAN 11.2). For example, if one field holds 4000 characters but the total stops at 8000, import uses two or three fields per sync and multi-part import for larger Aegis files.
+- **C** decides whether `otpauth://` URIs can be pasted as they are on your phone and whether the parser needs the `;` fallback (PROJECT_PLAN 11.2).
+- **D** decides whether status messages and cleared secret fields are visible on the phone. The design works either way (forms are idempotent and results also show on the watch, PROJECT_PLAN 10.3); this tells us which one you'll experience.
- **E and G** decide whether processing on `onSettingsChanged` is enough or whether the app must also process forms at every start (it will do both; this confirms it).
- **F** confirms what the password field protects (on-screen masking only).
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 14cd173..38bc112 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -4,7 +4,7 @@
-->
# Testing
-The full test plan is in [README section 17](../README.md#17-test-plan). This document tracks what exists, how to run it, and the latest results. Every check runs in Docker; `make ci` runs them all in order and stops at the first failure.
+The full test plan is in [PROJECT_PLAN section 17](PROJECT_PLAN.md#17-test-plan). This document tracks what exists, how to run it, and the latest results. Every check runs in Docker; `make ci` runs them all in order and stops at the first failure.
## Test layers
@@ -56,7 +56,7 @@ The UI flow now has two scenarios, 42 to 44 screens per watch:
What the new flow found and what was fixed:
- "Entry IDs" with 110 entries tripped the watchdog (all headers read in one event); it now reads 10 per step and shows "Reading…". The debug benchmark times at most 20 headers.
-- Auto-lock didn't close a locked code card (README 7.5); it does now.
+- Auto-lock didn't close a locked code card (PROJECT_PLAN 7.5); it does now.
- The list said "Confirm in menu" for a queued Require PIN that applies by itself; locks are no longer counted there.
- A locked code card showed the barcode icon instead of the padlock.
- Without the OK key the keypad's bottom row looked like a missing key; it is now empty, 0, delete, as on a phone.
@@ -92,8 +92,8 @@ Verification: `make screenshots TEST_DEVICES=fr70 SPOT_DEVICES=` passes (7 min 5
Monkey C unit tests: 154 per device (34 new, in `test/settings/`), all passing on the four test devices.
-- **Add OTP:** every type (TOTP, HOTP with its counter, Steam ignoring algorithm, digits and period), and each validation message of README 10.4: Base32 character and position, `=` padding, missing secret, digits, period, algorithm, type, counter, control characters, length in bytes, a missing name, the warning below 80 bits. After a problem only the secret is cleared; after success the whole form, with the switch off.
-- **Add code:** a valid payload for each of the 12 symbologies (2D codes come out cached and decodable) and an invalid one for each, with the encoder's message (check digit, length, character and position, capacity). Options and "Fix if wrong" are stored as sent, and codes too large for the screen are added with the README 12.2 warning.
+- **Add OTP:** every type (TOTP, HOTP with its counter, Steam ignoring algorithm, digits and period), and each validation message of PROJECT_PLAN 10.4: Base32 character and position, `=` padding, missing secret, digits, period, algorithm, type, counter, control characters, length in bytes, a missing name, the warning below 80 bits. After a problem only the secret is cleared; after success the whole form, with the switch off.
+- **Add code:** a valid payload for each of the 12 symbologies (2D codes come out cached and decodable) and an invalid one for each, with the encoder's message (check digit, length, character and position, capacity). Options and "Fix if wrong" are stored as sent, and codes too large for the screen are added with the PROJECT_PLAN 12.2 warning.
- **Manage entry:** every action on unlocked entries; locked entries' changes queued even while unlocked, then applied by confirmation; "Require PIN" applied at once or queued; unknown, malformed, pending and damaged IDs; a queued change whose entry vanished; changes arriving while the queue is applied; tampered queue items dropped; the 16-item limit.
- **Idempotency:** the same Add OTP, Manage and Import form twice is applied once; any change makes a new request.
- **The whole run:** all forms at once in README order; the About catalog, its 800-character cut and the storage line; "Encrypt names" on and off, with locked entries converted after the PIN; work detected only when needed.
@@ -111,11 +111,11 @@ The flow caught one watchdog trip on the Forerunner 255S that the unit tests can
Monkey C unit tests: 120 per device.
-1. **Reference comparison.** 24 Aztec symbols are identical to aztec_code_generator: compact (15 x 15 up) and full range, including sizes with reference grids, and error correction of 10, 23, 36 and 50 %. The payloads are of one character class each, for which both encoders produce the same bits; README 12.4 explains why mixed content can't be compared exactly.
+1. **Reference comparison.** 24 Aztec symbols are identical to aztec_code_generator: compact (15 x 15 up) and full range, including sizes with reference grids, and error correction of 10, 23, 36 and 50 %. The payloads are of one character class each, for which both encoders produce the same bits; PROJECT_PLAN 12.4 explains why mixed content can't be compared exactly.
2. **Decoding.** 8 Aztec codes are decoded with zxing-cpp: mixed case with digits and punctuation (Binary Shift and mode latches), UTF-8, a URL, and a long payload.
3. **Screenshots.** The UI flow opens an Aztec card after the PDF417 card, and its screenshot decodes on all seven watches.
-With that, all 12 symbologies of README 12.1 pass checks 1 to 3. The UI flow shows 16 screens per watch (15 on touch-only watches), five of them barcodes that must decode.
+With that, all 12 symbologies of PROJECT_PLAN 12.1 pass checks 1 to 3. The UI flow shows 16 screens per watch (15 on touch-only watches), five of them barcodes that must decode.
### M6c, 2026-10-20 (Data Matrix and PDF417, in 0.6.0)
@@ -131,7 +131,7 @@ Monkey C unit tests: 117 per device.
Found and fixed along the way:
-- **ppf padding bug:** see README 12.4.
+- **ppf padding bug:** see PROJECT_PLAN 12.4.
- **Compiler out of memory:** one long `&&` chain of `instanceof` tests in a test made the Monkey C compiler run out of memory (more than 6 GB), and splitting the chain fixed it.
- **Watchdog on the 255S:** packing an 86 x 87 PDF417 matrix for the cache in the same event as the encoder's last step tripped the watchdog. PDF417 rows are now stored once with a row height of 3, packing does 8 modules per loop, and the cache is saved in its own timer event.
@@ -141,7 +141,7 @@ Monkey C unit tests: 111 per device.
1. **Reference comparison with segno** (`make codes`).
- **31 matrices identical module for module.** They cover versions 1 to 40 and levels L, M, Q and H, numeric, alphanumeric and UTF-8 byte data, forced versions and masks, and automatic choices. The watch picks the same version and mask as segno with `boost_error=False`.
- - **Two deliberate differences**, listed in README 12.4:
+ - **Two deliberate differences**, listed in PROJECT_PLAN 12.4:
- segno pads a stream that already ends on a codeword boundary with an extra zero byte; the watch follows ISO 18004.
- segno uses a single mode per symbol; the watch segments optimally.
2. **Decoding.** 30 QR codes from the test run are decoded with zxing-cpp at 2 and 3 px per module. They include mixed segmented content, Unicode, emoji, a Wi-Fi string and all four levels.
@@ -156,7 +156,7 @@ What the UI flow found: scoring the eight masks tripped the watchdog, first on t
### M6a, 2026-10-20 (linear barcodes, in 0.6.0)
-Monkey C unit tests: 105 per device, with the checks of README 12.4.
+Monkey C unit tests: 105 per device, with the checks of PROJECT_PLAN 12.4.
1. **Reference comparison.** 217 rows from zxing-cpp (`tools/gen_code_vectors.py`, `make codes`): fixed edge cases and seeded random payloads for every linear symbology. EAN-13, EAN-8, UPC-A, UPC-E, Code 39 (with and without the check character), ITF and Codabar match module for module. Code 128 matches in length. zxing-cpp 3's zint backend picks a different one of the equally short encodings; on 5,000 random strings the lengths were always equal.
2. **Decoding.** Every Code 128 reference row, plus 139 codes for seeded random payloads in all eight symbologies, is dumped by the test run. `tools/check_codes.py` renders each with its quiet zone at 1, 2 and 3 px per module and decodes it with zxing-cpp, on each test device: 169 codes, all decoded.
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
index 67eddbd..392e35e 100644
--- a/docs/USER_GUIDE.md
+++ b/docs/USER_GUIDE.md
@@ -190,7 +190,7 @@ OTP Wallet never exports your secrets: they only flow into the watch. Your backu
- **Security**
- Locked entries are only as strong as the PIN against someone who copies the watch's storage; unlocked entries rely on the watch keeping its storage private.
- Secrets and the import text pass through Garmin Connect's settings on your phone.
- - See [SECURITY.md](SECURITY.md) and [README section 8](../README.md#8-threat-model).
+ - See [SECURITY.md](SECURITY.md) and [PROJECT_PLAN section 8](PROJECT_PLAN.md#8-threat-model).
- **Barcodes**
- Data Matrix uses ASCII and Base256 encoding only (no C40, Text, X12 or EDIFACT), and Aztec a simple mode choice. Their symbols are valid, but sometimes larger than another encoder's.
- PDF417 only fits short contents on small round screens (about 170 codewords on a 218-pixel screen).
diff --git a/resources/pdf417/codes0.json b/resources/pdf417/codes0.json
index fa060e7..c0edd85 100644
--- a/resources/pdf417/codes0.json
+++ b/resources/pdf417/codes0.json
@@ -1 +1 @@
-{"_author":"cirakg & claude ai","_summary":"PDF417 cluster 0 patterns (generated; do not edit).","v":[120256,125680,128380,120032,125560,128318,108736,119920,108640,86080,108592,86048,110016,120560,125820,109792,120440,125758,88256,109680,88160,89536,110320,120700,89312,110200,120638,89200,110140,89840,110460,89720,110398,89980,128506,119520,125304,128190,107712,119408,125244,107616,119352,84032,107568,119324,84000,107544,83984,108256,119672,125374,85184,108144,119612,85088,108088,119582,85040,108060,85728,108408,119742,85616,108348,85560,108318,85880,108478,85820,85790,107200,119152,125116,107104,119096,125086,83008,107056,119068,82976,107032,82960,82952,83648,107376,119228,83552,107320,119198,83504,107292,83480,83468,83824,107452,83768,107422,83740,83900,106848,118968,125022,82496,106800,118940,82464,106776,118926,82448,106764,82440,106758,82784,106936,119006,82736,106908,82712,106894,82700,82694,106974,82830,82240,106672,118876,82208,106648,118862,82192,106636,82184,106630,82180,82352,82328,82316,82080,118830,106572,106566,82050,117472,124280,127678,103616,117360,124220,103520,117304,124190,75840,103472,75808,104160,117624,124350,76992,104048,117564,76896,103992,76848,76824,77536,104312,117694,77424,104252,77368,77340,77688,104382,77628,77758,121536,126320,128700,121440,126264,128670,111680,121392,126236,111648,121368,126222,111632,121356,103104,117104,124092,112320,103008,117048,124062,112224,121656,126366,93248,74784,102936,117006,93216,112152,93200,75456,103280,117180,93888,75360,103224,117150,93792,112440,121758,93744,75288,93720,75632,103356,94064,75576,103326,94008,112542,93980,75708,94140,75678,94110,121184,126136,128606,111168,121136,126108,111136,121112,126094,111120,121100,111112,111108,102752,116920,123998,111456,102704,116892,91712,74272,121244,116878,91680,74256,102668,91664,111372,102662,74244,74592,102840,116958,92000,74544,102812,91952,111516,102798,91928,74508,74502,74680,102878,92088,74652,92060,74638,92046,92126,110912,121008,126044,110880,120984,126030,110864,120972,110856,120966,110852,110850,74048,102576,116828,90944,74016,102552,116814,90912,111000,121038,90896,73992,102534,90888,110982,90884,74160,102620,91056,74136,102606,91032,111054,91020,74118,91014,91100,91086,110752,120920,125998,110736,120908,110728,120902,110724,110722,73888,102488,116782,90528,73872,102476,90512,110796,102470,90504,73860,90500,73858,73944,90584,90572,90566,120876,120870,110658,102444,73800,90312,90308,90306,101056,116080,123580,100960,116024,70720,100912,115996,70688,100888,70672,70664,71360,101232,116156,71264,101176,116126,71216,101148,71192,71180,71536,101308,71480,101278,71452,71612,71582,118112,124600,127838,105024,118064,124572,104992,118040,124558,104976,118028,104968,118022,100704,115896,123486,105312,100656,115868,79424,70176,118172,115854,79392,105240,100620,79376,70152,79368,70496,100792,115934,79712,70448,118238,79664,105372,100750,79640,70412,79628,70584,100830,79800,70556,79772,70542,70622,79838,122176,126640,128860,122144,126616,128846,122128,126604,122120,126598,122116,104768,117936,124508,113472,104736,126684,124494,113440,122264,126670,113424,104712,117894,113416,122246,104706,69952,100528,115804,78656,69920,100504,115790,96064,78624,104856,117966,96032,113560,122318,100486,96016,78600,104838,96008,69890,70064,100572,78768,70040,100558,96176,78744,104910,96152,113614,70022,78726,70108,78812,70094,96220,78798,122016,126552,128814,122000,126540,121992,126534,121988,121986,104608,117848,124462,113056,104592,126574,113040,122060,117830,113032,104580,113028,104578,113026,69792,100440,115758,78240,69776,100428,95136,78224,104652,100422,95120,113100,69764,95112,78212,69762,78210,69848,100462,78296,69836,95192,78284,69830,95180,78278,69870,95214,121936,126508,121928,126502,121924,121922,104528,117804,112848,104520,117798,112840,121958,112836,104514,112834,69712,100396,78032,69704,100390,94672,78024,104550,94664,112870,69698,94660,78018,94658,78060,94700,94694,126486,121890,117782,104484,104482,69672,77928,94440,69666,77922,99680,68160,99632,68128,99608,115342,68112,99596,68104,99590,68448,99768,115422,68400,99740,68376,99726,68364,68358,68536,99806,68508,68494,68574,101696,116400,123740,101664,116376,101648,116364,101640,116358,101636,67904,99504,115292,72512,67872,116444,115278,72480,101784,116430,72464,67848,99462,72456,101766,67842,68016,99548,72624,67992,99534,72600,101838,72588,67974,68060,72668,68046,72654,118432,124760,127918,118416,124748,118408,124742,118404,118402,101536,116312,105888,101520,116300,105872,118476,116294,105864,101508,105860,101506,105858,67744,99416,72096,67728,116334,80800,72080,101580,99398,80784,105932,67716,80776,72068,67714,72066,67800,99438,72152,67788,80856,72140,67782,80844,72134,67822,72174,80878,126800,128940,126792,128934,126788,126786,118352,124716,122576,126828,124710,122568,126822,122564,118338,122562,101456,116268,105680,101448,116262,114128,105672,118374,114120,122598,101442,114116,105666,114114,67664,99372,71888,67656,99366,80336,71880,101478,97232,80328,105702,67650,97224,114150,71874,97220,67692,71916,67686,80364,71910,97260,80358,97254,126760,128918,126756,126754,118312,124694,122472,126774,122468,118306,122466,101416,116246,105576,101412,113896,105572,101410,113892,105570,113890,67624,99350,71784,101430,80104,71780,67618,96744,80100,71778,96740,80098,96738,71798,96758,126738,122420,122418,105524,113780,113778,71732,79988,96500,96498,66880,66848,98968,66832,66824,66820,66992,66968,66956,66950,67036,67022,100000,99984,115532,99976,115526,99972,99970,66720,98904,69024,100056,98892,69008,100044,69000,100038,68996,66690,68994,66776,98926,69080,100078,69068,66758,69062,66798,69102,116560,116552,116548,116546,99920,102096,116588,115494,102088,116582,102084,99906,102082,66640,68816,66632,98854,73168,68808,66628,73160,68804,66626,73156,68802,66668,68844,66662,73196,68838,73190,124840,124836,124834,116520,118632,124854,118628,116514,118626,99880,115478,101992,116534,106216,101988,99874,106212,101986,106210,66600,98838,68712,99894,72936,68708,66594,81384,72932,68706,81380,72930,66614,68726,72950,81398,128980,128978,124820,126900,124818,126898,116500,118580,116498,122740,118578,122738,99860,101940,99858,106100,101938,114420]}
+{"_author":"cirakg & claude ai","_license":"Derived from pdf417gen (MIT); see THIRD_PARTY_NOTICES.md and LICENSES/pdf417gen.txt.","_summary":"PDF417 cluster 0 patterns (generated; do not edit).","v":[120256,125680,128380,120032,125560,128318,108736,119920,108640,86080,108592,86048,110016,120560,125820,109792,120440,125758,88256,109680,88160,89536,110320,120700,89312,110200,120638,89200,110140,89840,110460,89720,110398,89980,128506,119520,125304,128190,107712,119408,125244,107616,119352,84032,107568,119324,84000,107544,83984,108256,119672,125374,85184,108144,119612,85088,108088,119582,85040,108060,85728,108408,119742,85616,108348,85560,108318,85880,108478,85820,85790,107200,119152,125116,107104,119096,125086,83008,107056,119068,82976,107032,82960,82952,83648,107376,119228,83552,107320,119198,83504,107292,83480,83468,83824,107452,83768,107422,83740,83900,106848,118968,125022,82496,106800,118940,82464,106776,118926,82448,106764,82440,106758,82784,106936,119006,82736,106908,82712,106894,82700,82694,106974,82830,82240,106672,118876,82208,106648,118862,82192,106636,82184,106630,82180,82352,82328,82316,82080,118830,106572,106566,82050,117472,124280,127678,103616,117360,124220,103520,117304,124190,75840,103472,75808,104160,117624,124350,76992,104048,117564,76896,103992,76848,76824,77536,104312,117694,77424,104252,77368,77340,77688,104382,77628,77758,121536,126320,128700,121440,126264,128670,111680,121392,126236,111648,121368,126222,111632,121356,103104,117104,124092,112320,103008,117048,124062,112224,121656,126366,93248,74784,102936,117006,93216,112152,93200,75456,103280,117180,93888,75360,103224,117150,93792,112440,121758,93744,75288,93720,75632,103356,94064,75576,103326,94008,112542,93980,75708,94140,75678,94110,121184,126136,128606,111168,121136,126108,111136,121112,126094,111120,121100,111112,111108,102752,116920,123998,111456,102704,116892,91712,74272,121244,116878,91680,74256,102668,91664,111372,102662,74244,74592,102840,116958,92000,74544,102812,91952,111516,102798,91928,74508,74502,74680,102878,92088,74652,92060,74638,92046,92126,110912,121008,126044,110880,120984,126030,110864,120972,110856,120966,110852,110850,74048,102576,116828,90944,74016,102552,116814,90912,111000,121038,90896,73992,102534,90888,110982,90884,74160,102620,91056,74136,102606,91032,111054,91020,74118,91014,91100,91086,110752,120920,125998,110736,120908,110728,120902,110724,110722,73888,102488,116782,90528,73872,102476,90512,110796,102470,90504,73860,90500,73858,73944,90584,90572,90566,120876,120870,110658,102444,73800,90312,90308,90306,101056,116080,123580,100960,116024,70720,100912,115996,70688,100888,70672,70664,71360,101232,116156,71264,101176,116126,71216,101148,71192,71180,71536,101308,71480,101278,71452,71612,71582,118112,124600,127838,105024,118064,124572,104992,118040,124558,104976,118028,104968,118022,100704,115896,123486,105312,100656,115868,79424,70176,118172,115854,79392,105240,100620,79376,70152,79368,70496,100792,115934,79712,70448,118238,79664,105372,100750,79640,70412,79628,70584,100830,79800,70556,79772,70542,70622,79838,122176,126640,128860,122144,126616,128846,122128,126604,122120,126598,122116,104768,117936,124508,113472,104736,126684,124494,113440,122264,126670,113424,104712,117894,113416,122246,104706,69952,100528,115804,78656,69920,100504,115790,96064,78624,104856,117966,96032,113560,122318,100486,96016,78600,104838,96008,69890,70064,100572,78768,70040,100558,96176,78744,104910,96152,113614,70022,78726,70108,78812,70094,96220,78798,122016,126552,128814,122000,126540,121992,126534,121988,121986,104608,117848,124462,113056,104592,126574,113040,122060,117830,113032,104580,113028,104578,113026,69792,100440,115758,78240,69776,100428,95136,78224,104652,100422,95120,113100,69764,95112,78212,69762,78210,69848,100462,78296,69836,95192,78284,69830,95180,78278,69870,95214,121936,126508,121928,126502,121924,121922,104528,117804,112848,104520,117798,112840,121958,112836,104514,112834,69712,100396,78032,69704,100390,94672,78024,104550,94664,112870,69698,94660,78018,94658,78060,94700,94694,126486,121890,117782,104484,104482,69672,77928,94440,69666,77922,99680,68160,99632,68128,99608,115342,68112,99596,68104,99590,68448,99768,115422,68400,99740,68376,99726,68364,68358,68536,99806,68508,68494,68574,101696,116400,123740,101664,116376,101648,116364,101640,116358,101636,67904,99504,115292,72512,67872,116444,115278,72480,101784,116430,72464,67848,99462,72456,101766,67842,68016,99548,72624,67992,99534,72600,101838,72588,67974,68060,72668,68046,72654,118432,124760,127918,118416,124748,118408,124742,118404,118402,101536,116312,105888,101520,116300,105872,118476,116294,105864,101508,105860,101506,105858,67744,99416,72096,67728,116334,80800,72080,101580,99398,80784,105932,67716,80776,72068,67714,72066,67800,99438,72152,67788,80856,72140,67782,80844,72134,67822,72174,80878,126800,128940,126792,128934,126788,126786,118352,124716,122576,126828,124710,122568,126822,122564,118338,122562,101456,116268,105680,101448,116262,114128,105672,118374,114120,122598,101442,114116,105666,114114,67664,99372,71888,67656,99366,80336,71880,101478,97232,80328,105702,67650,97224,114150,71874,97220,67692,71916,67686,80364,71910,97260,80358,97254,126760,128918,126756,126754,118312,124694,122472,126774,122468,118306,122466,101416,116246,105576,101412,113896,105572,101410,113892,105570,113890,67624,99350,71784,101430,80104,71780,67618,96744,80100,71778,96740,80098,96738,71798,96758,126738,122420,122418,105524,113780,113778,71732,79988,96500,96498,66880,66848,98968,66832,66824,66820,66992,66968,66956,66950,67036,67022,100000,99984,115532,99976,115526,99972,99970,66720,98904,69024,100056,98892,69008,100044,69000,100038,68996,66690,68994,66776,98926,69080,100078,69068,66758,69062,66798,69102,116560,116552,116548,116546,99920,102096,116588,115494,102088,116582,102084,99906,102082,66640,68816,66632,98854,73168,68808,66628,73160,68804,66626,73156,68802,66668,68844,66662,73196,68838,73190,124840,124836,124834,116520,118632,124854,118628,116514,118626,99880,115478,101992,116534,106216,101988,99874,106212,101986,106210,66600,98838,68712,99894,72936,68708,66594,81384,72932,68706,81380,72930,66614,68726,72950,81398,128980,128978,124820,126900,124818,126898,116500,118580,116498,122740,118578,122738,99860,101940,99858,106100,101938,114420]}
diff --git a/resources/pdf417/codes1.json b/resources/pdf417/codes1.json
index 941aca3..bf9f023 100644
--- a/resources/pdf417/codes1.json
+++ b/resources/pdf417/codes1.json
@@ -1 +1 @@
-{"_author":"cirakg & claude ai","_summary":"PDF417 cluster 3 patterns (generated; do not edit).","v":[128352,129720,125504,128304,129692,125472,128280,129678,125456,128268,125448,128262,125444,125792,128440,129758,120384,125744,128412,120352,125720,128398,120336,125708,120328,125702,120324,120672,125880,128478,110144,120624,125852,110112,120600,125838,110096,120588,110088,120582,110084,110432,120760,125918,89664,110384,120732,89632,110360,120718,89616,110348,89608,110342,89952,110520,120798,89904,110492,89880,110478,89868,90040,110558,90012,89998,125248,128176,129628,125216,128152,129614,125200,128140,125192,128134,125188,125186,119616,125360,128220,119584,125336,128206,119568,125324,119560,125318,119556,119554,108352,119728,125404,108320,119704,125390,108304,119692,108296,119686,108292,108290,85824,108464,119772,85792,108440,119758,85776,108428,85768,108422,85764,85936,108508,85912,108494,85900,85894,85980,85966,125088,128088,129582,125072,128076,125064,128070,125060,125058,119200,125144,128110,119184,125132,119176,125126,119172,119170,107424,119256,125166,107408,119244,107400,119238,107396,107394,83872,107480,119278,83856,107468,83848,107462,83844,83842,83928,107502,83916,83910,83950,125008,128044,125000,128038,124996,124994,118992,125036,118984,125030,118980,118978,106960,119020,106952,119014,106948,106946,82896,106988,82888,106982,82884,82882,82924,82918,124968,128022,124964,124962,118888,124982,118884,118882,106728,118902,106724,106722,82408,106742,82404,82402,124948,124946,118836,118834,106612,106610,124224,127664,129372,124192,127640,129358,124176,127628,124168,127622,124164,124162,117568,124336,127708,117536,124312,127694,117520,124300,117512,124294,117508,117506,104256,117680,124380,104224,117656,124366,104208,117644,104200,117638,104196,104194,77632,104368,117724,77600,104344,117710,77584,104332,77576,104326,77572,77744,104412,77720,104398,77708,77702,77788,77774,128672,129880,93168,128656,129868,92664,128648,129862,92412,128644,128642,124064,127576,129326,126368,124048,129902,126352,128716,127558,126344,124036,126340,124034,126338,117152,124120,127598,121760,117136,124108,121744,126412,124102,121736,117124,121732,117122,121730,103328,117208,124142,112544,103312,117196,112528,121804,117190,112520,103300,112516,103298,112514,75680,103384,117230,94112,75664,103372,94096,112588,103366,94088,75652,94084,75650,75736,103406,94168,75724,94156,75718,94150,75758,128592,129836,91640,128584,129830,91388,128580,91262,128578,123984,127532,126160,123976,127526,126152,128614,126148,123970,126146,116944,124012,121296,116936,124006,121288,126182,121284,116930,121282,102864,116972,111568,102856,116966,111560,121318,111556,102850,111554,74704,102892,92112,74696,102886,92104,111590,92100,74690,92098,74732,92140,74726,92134,128552,129814,90876,128548,90750,128546,123944,127510,126056,128566,126052,123938,126050,116840,123958,121064,116836,121060,116834,121058,102632,116854,111080,121078,111076,102626,111074,74216,102646,91112,74212,91108,74210,91106,74230,91126,128532,90494,128530,123924,126004,123922,126002,116788,120948,116786,120946,102516,110836,102514,110834,73972,90612,73970,90610,128522,123914,125978,116762,120890,102458,110714,123552,127320,129198,123536,127308,123528,127302,123524,123522,116128,123608,127342,116112,123596,116104,123590,116100,116098,101280,116184,123630,101264,116172,101256,116166,101252,101250,71584,101336,116206,71568,101324,71560,101318,71556,71554,71640,101358,71628,71622,71662,127824,129452,79352,127816,129446,79100,127812,78974,127810,123472,127276,124624,123464,127270,124616,127846,124612,123458,124610,115920,123500,118224,115912,123494,118216,124646,118212,115906,118210,100816,115948,105424,100808,115942,105416,118246,105412,100802,105410,70608,100844,79824,70600,100838,79816,105446,79812,70594,79810,70636,79852,70630,79846,129960,95728,113404,129956,95480,113278,129954,95356,95294,127784,129430,78588,128872,129974,95996,78462,128868,127778,95870,128866,123432,127254,124520,123428,126696,128886,123426,126692,124514,126690,115816,123446,117992,115812,122344,117988,115810,122340,117986,122338,100584,115830,104936,100580,113640,104932,100578,113636,104930,113634,70120,100598,78824,70116,96232,78820,70114,96228,78818,96226,70134,78838,129940,94968,113022,129938,94844,94782,127764,78206,128820,127762,95102,128818,123412,124468,123410,126580,124466,126578,115764,117876,115762,122100,117874,122098,100468,104692,100466,113140,104690,113138,69876,78324,69874,95220,78322,95218,129930,94588,94526,127754,128794,123402,124442,126522,115738,117818,121978,100410,104570,112890,69754,78074,94714,94398,123216,127148,123208,127142,123204,123202,115408,123244,115400,123238,115396,115394,99792,115436,99784,115430,99780,99778,68560,99820,68552,99814,68548,68546,68588,68582,127400,129238,72444,127396,72318,127394,123176,127126,123752,123172,123748,123170,123746,115304,123190,116456,115300,116452,115298,116450,99560,115318,101864,99556,101860,99554,101858,68072,99574,72680,68068,72676,68066,72674,68086,72694,129492,80632,105854,129490,80508,80446,127380,72062,127924,127378,80766,127922,123156,123700,123154,124788,123698,124786,115252,116340,115250,118516,116338,118514,99444,101620,99442,105972,101618,105970,67828,72180,67826,80884,72178,80882,97008,114044,96888,113982,96828,96798,129482,80252,130010,97148,80190,97086,127370,127898,128954,123146,123674,124730,126842,115226,116282,118394,122618,99386,101498,105722,114170,67706,71930,80378,96632,113854,96572,96542,80062,96702,96444,96414,96350,123048,123044,123042,115048,123062,115044,115042,99048,115062,99044,99042,67048,99062,67044,67042,67062,127188,68990,127186,123028,123316,123026,123314,114996,115572,114994,115570,98932,100084,98930,100082,66804,69108,66802,69106,129258,73084,73022,127178,127450,123018,123290,123834,114970,115514,116602,98874,99962,102138,66682,68858,73210,81272,106174,81212,81182,72894,81342,97648,114364,97592,114334,97564,97550,81084,97724,81054,97694,97464,114270,97436,97422,80990,97502,97372,97358,97326,114868,114866,98676,98674,66292,66290,123098,114842,115130,98618,99194,66170,67322,69310,73404,73374,81592,106334,81564,81550,73310,81630,97968,114524,97944,114510,97932,97926,81500,98012,81486,97998,97880,114478,97868,97862,81454,97902,97836,97830,69470,73564,73550,81752,106414,81740,81734,73518,81774,81708,81702]}
+{"_author":"cirakg & claude ai","_license":"Derived from pdf417gen (MIT); see THIRD_PARTY_NOTICES.md and LICENSES/pdf417gen.txt.","_summary":"PDF417 cluster 3 patterns (generated; do not edit).","v":[128352,129720,125504,128304,129692,125472,128280,129678,125456,128268,125448,128262,125444,125792,128440,129758,120384,125744,128412,120352,125720,128398,120336,125708,120328,125702,120324,120672,125880,128478,110144,120624,125852,110112,120600,125838,110096,120588,110088,120582,110084,110432,120760,125918,89664,110384,120732,89632,110360,120718,89616,110348,89608,110342,89952,110520,120798,89904,110492,89880,110478,89868,90040,110558,90012,89998,125248,128176,129628,125216,128152,129614,125200,128140,125192,128134,125188,125186,119616,125360,128220,119584,125336,128206,119568,125324,119560,125318,119556,119554,108352,119728,125404,108320,119704,125390,108304,119692,108296,119686,108292,108290,85824,108464,119772,85792,108440,119758,85776,108428,85768,108422,85764,85936,108508,85912,108494,85900,85894,85980,85966,125088,128088,129582,125072,128076,125064,128070,125060,125058,119200,125144,128110,119184,125132,119176,125126,119172,119170,107424,119256,125166,107408,119244,107400,119238,107396,107394,83872,107480,119278,83856,107468,83848,107462,83844,83842,83928,107502,83916,83910,83950,125008,128044,125000,128038,124996,124994,118992,125036,118984,125030,118980,118978,106960,119020,106952,119014,106948,106946,82896,106988,82888,106982,82884,82882,82924,82918,124968,128022,124964,124962,118888,124982,118884,118882,106728,118902,106724,106722,82408,106742,82404,82402,124948,124946,118836,118834,106612,106610,124224,127664,129372,124192,127640,129358,124176,127628,124168,127622,124164,124162,117568,124336,127708,117536,124312,127694,117520,124300,117512,124294,117508,117506,104256,117680,124380,104224,117656,124366,104208,117644,104200,117638,104196,104194,77632,104368,117724,77600,104344,117710,77584,104332,77576,104326,77572,77744,104412,77720,104398,77708,77702,77788,77774,128672,129880,93168,128656,129868,92664,128648,129862,92412,128644,128642,124064,127576,129326,126368,124048,129902,126352,128716,127558,126344,124036,126340,124034,126338,117152,124120,127598,121760,117136,124108,121744,126412,124102,121736,117124,121732,117122,121730,103328,117208,124142,112544,103312,117196,112528,121804,117190,112520,103300,112516,103298,112514,75680,103384,117230,94112,75664,103372,94096,112588,103366,94088,75652,94084,75650,75736,103406,94168,75724,94156,75718,94150,75758,128592,129836,91640,128584,129830,91388,128580,91262,128578,123984,127532,126160,123976,127526,126152,128614,126148,123970,126146,116944,124012,121296,116936,124006,121288,126182,121284,116930,121282,102864,116972,111568,102856,116966,111560,121318,111556,102850,111554,74704,102892,92112,74696,102886,92104,111590,92100,74690,92098,74732,92140,74726,92134,128552,129814,90876,128548,90750,128546,123944,127510,126056,128566,126052,123938,126050,116840,123958,121064,116836,121060,116834,121058,102632,116854,111080,121078,111076,102626,111074,74216,102646,91112,74212,91108,74210,91106,74230,91126,128532,90494,128530,123924,126004,123922,126002,116788,120948,116786,120946,102516,110836,102514,110834,73972,90612,73970,90610,128522,123914,125978,116762,120890,102458,110714,123552,127320,129198,123536,127308,123528,127302,123524,123522,116128,123608,127342,116112,123596,116104,123590,116100,116098,101280,116184,123630,101264,116172,101256,116166,101252,101250,71584,101336,116206,71568,101324,71560,101318,71556,71554,71640,101358,71628,71622,71662,127824,129452,79352,127816,129446,79100,127812,78974,127810,123472,127276,124624,123464,127270,124616,127846,124612,123458,124610,115920,123500,118224,115912,123494,118216,124646,118212,115906,118210,100816,115948,105424,100808,115942,105416,118246,105412,100802,105410,70608,100844,79824,70600,100838,79816,105446,79812,70594,79810,70636,79852,70630,79846,129960,95728,113404,129956,95480,113278,129954,95356,95294,127784,129430,78588,128872,129974,95996,78462,128868,127778,95870,128866,123432,127254,124520,123428,126696,128886,123426,126692,124514,126690,115816,123446,117992,115812,122344,117988,115810,122340,117986,122338,100584,115830,104936,100580,113640,104932,100578,113636,104930,113634,70120,100598,78824,70116,96232,78820,70114,96228,78818,96226,70134,78838,129940,94968,113022,129938,94844,94782,127764,78206,128820,127762,95102,128818,123412,124468,123410,126580,124466,126578,115764,117876,115762,122100,117874,122098,100468,104692,100466,113140,104690,113138,69876,78324,69874,95220,78322,95218,129930,94588,94526,127754,128794,123402,124442,126522,115738,117818,121978,100410,104570,112890,69754,78074,94714,94398,123216,127148,123208,127142,123204,123202,115408,123244,115400,123238,115396,115394,99792,115436,99784,115430,99780,99778,68560,99820,68552,99814,68548,68546,68588,68582,127400,129238,72444,127396,72318,127394,123176,127126,123752,123172,123748,123170,123746,115304,123190,116456,115300,116452,115298,116450,99560,115318,101864,99556,101860,99554,101858,68072,99574,72680,68068,72676,68066,72674,68086,72694,129492,80632,105854,129490,80508,80446,127380,72062,127924,127378,80766,127922,123156,123700,123154,124788,123698,124786,115252,116340,115250,118516,116338,118514,99444,101620,99442,105972,101618,105970,67828,72180,67826,80884,72178,80882,97008,114044,96888,113982,96828,96798,129482,80252,130010,97148,80190,97086,127370,127898,128954,123146,123674,124730,126842,115226,116282,118394,122618,99386,101498,105722,114170,67706,71930,80378,96632,113854,96572,96542,80062,96702,96444,96414,96350,123048,123044,123042,115048,123062,115044,115042,99048,115062,99044,99042,67048,99062,67044,67042,67062,127188,68990,127186,123028,123316,123026,123314,114996,115572,114994,115570,98932,100084,98930,100082,66804,69108,66802,69106,129258,73084,73022,127178,127450,123018,123290,123834,114970,115514,116602,98874,99962,102138,66682,68858,73210,81272,106174,81212,81182,72894,81342,97648,114364,97592,114334,97564,97550,81084,97724,81054,97694,97464,114270,97436,97422,80990,97502,97372,97358,97326,114868,114866,98676,98674,66292,66290,123098,114842,115130,98618,99194,66170,67322,69310,73404,73374,81592,106334,81564,81550,73310,81630,97968,114524,97944,114510,97932,97926,81500,98012,81486,97998,97880,114478,97868,97862,81454,97902,97836,97830,69470,73564,73550,81752,106414,81740,81734,73518,81774,81708,81702]}
diff --git a/resources/pdf417/codes2.json b/resources/pdf417/codes2.json
index d189aea..cd40a41 100644
--- a/resources/pdf417/codes2.json
+++ b/resources/pdf417/codes2.json
@@ -1 +1 @@
-{"_author":"cirakg & claude ai","_summary":"PDF417 cluster 6 patterns (generated; do not edit).","v":[109536,120312,86976,109040,120060,86496,108792,119934,86256,108668,86136,129744,89056,110072,129736,88560,109820,129732,88312,109694,129730,88188,128464,129772,89592,128456,129766,89340,128452,89214,128450,125904,128492,125896,128486,125892,125890,120784,125932,120776,125926,120772,120770,110544,120812,110536,120806,110532,84928,108016,119548,84448,107768,119422,84208,107644,84088,107582,84028,129640,85488,108284,129636,85240,108158,129634,85116,85054,128232,129654,85756,128228,85630,128226,125416,128246,125412,125410,119784,125430,119780,119778,108520,119798,108516,108514,83424,107256,119166,83184,107132,83064,107070,83004,82974,129588,83704,107390,129586,83580,83518,128116,83838,128114,125172,125170,119284,119282,107508,107506,82672,106876,82552,106814,82492,82462,129562,82812,82750,128058,125050,119034,82296,106686,82236,82206,82366,82108,82078,76736,103920,117500,76256,103672,117374,76016,103548,75896,103486,75836,129384,77296,104188,129380,77048,104062,129378,76924,76862,127720,129398,77564,127716,77438,127714,124392,127734,124388,124386,117736,124406,117732,117730,104424,117750,104420,104418,112096,121592,126334,92608,111856,121468,92384,111736,121406,92272,111676,92216,111646,92188,75232,103160,117118,93664,74992,103036,93424,112252,102974,93304,74812,93244,74782,93214,129332,75512,103294,129908,129330,93944,75388,129906,93820,75326,93758,127604,75646,128756,127602,94078,128754,124148,126452,124146,126450,117236,121844,117234,121842,103412,103410,91584,111344,121212,91360,111224,121150,91248,111164,91192,111134,91164,91150,74480,102780,91888,74360,102718,91768,111422,91708,74270,91678,129306,74620,129850,92028,74558,91966,127546,128634,124026,126202,116986,121338,102906,90848,110968,121022,90736,110908,90680,110878,90652,90638,74104,102590,91000,74044,90940,74014,90910,74174,91070,90480,110780,90424,110750,90396,90382,73916,90556,73886,90526,90296,110686,90268,90254,73822,90334,90204,90190,71136,101112,116094,70896,100988,70776,100926,70716,70686,129204,71416,101246,129202,71292,71230,127348,71550,127346,123636,123634,116212,116210,101364,101362,79296,105200,118140,79072,105080,118078,78960,105020,78904,104990,78876,78862,70384,100732,79600,70264,100670,79480,105278,79420,70174,79390,129178,70524,129466,79740,70462,79678,127290,127866,123514,124666,115962,118266,100858,113376,122232,126654,95424,113264,122172,95328,113208,122142,95280,113180,95256,113166,95244,78560,104824,117950,95968,78448,104764,95856,113468,104734,95800,78364,95772,78350,95758,70008,100542,78712,69948,96120,78652,69918,96060,78622,96030,70078,78782,96190,94912,113008,122044,94816,112952,122014,94768,112924,94744,112910,94732,94726,78192,104636,95088,78136,104606,95032,113054,95004,78094,94990,69820,78268,69790,95164,78238,95134,94560,112824,121950,94512,112796,94488,112782,94476,94470,78008,104542,94648,77980,94620,77966,94606,69726,78046,94686,94384,112732,94360,112718,94348,94342,77916,94428,77902,94414,94296,112686,94284,94278,77870,94318,94252,94246,68336,99708,68216,99646,68156,68126,68476,68414,127162,123258,115450,99834,72416,101752,116414,72304,101692,72248,101662,72220,72206,67960,99518,72568,67900,72508,67870,72478,68030,72638,80576,105840,118460,80480,105784,118430,80432,105756,80408,105742,80396,80390,72048,101564,80752,71992,101534,80696,71964,80668,71950,80654,67772,72124,67742,80828,72094,80798,114016,122552,126814,96832,113968,122524,96800,113944,122510,96784,113932,96776,113926,96772,80224,105656,118366,97120,80176,105628,97072,114076,105614,97048,80140,97036,80134,97030,71864,101470,80312,71836,97208,80284,71822,97180,80270,97166,67678,71902,80350,97246,96576,113840,122460,96544,113816,122446,96528,113804,96520,113798,96516,96514,80048,105564,96688,80024,105550,96664,113870,96652,80006,96646,71772,80092,71758,96732,80078,96718,96416,113752,122414,96400,113740,96392,113734,96388,96386,79960,105518,96472,79948,96460,79942,96454,71726,79982,96494,96336,113708,96328,113702,96324,96322,79916,96364,79910,96358,96296,113686,96292,96290,79894,96310,66936,99006,66876,66846,67006,68976,100028,68920,99998,68892,68878,66748,69052,66718,69022,73056,102072,116574,73008,102044,72984,102030,72972,72966,68792,99934,73144,68764,73116,68750,73102,66654,68830,73182,81216,106160,118620,81184,106136,118606,81168,106124,81160,106118,81156,81154,72880,101980,81328,72856,101966,81304,106190,81292,72838,81286,68700,72924,68686,81372,72910,81358,114336,122712,126894,114320,122700,114312,122694,114308,114306,81056,106072,118574,97696,81040,106060,97680,114380,106054,97672,81028,97668,81026,97666,72792,101934,81112,72780,97752,81100,72774,97740,81094,97734,68654,72814,81134,97774,114256,122668,114248,122662,114244,114242,80976,106028,97488,80968,106022,97480,114278,97476,80962,97474,72748,81004,72742,97516,80998,97510,114216,122646,114212,114210,80936,106006,97384,80932,97380,80930,97378,72726,80950,97398,114196,114194,80916,97332,80914,97330,66236,66206,67256,99166,67228,67214,66142,67294,69296,100188,69272,100174,69260,69254,67164,69340,67150,69326,73376,102232,116654,73360,102220,73352,102214,73348,73346,69208,100142,73432,102254,73420,69190,73414,67118,69230,73454,106320,118700,106312,118694,106308,106306,73296,102188,81616,106348,102182,81608,73284,81604,73282,81602,69164,73324,69158,81644,73318,81638,122792,126934,122788,122786,106280,118678,114536,106276,114532,106274,114530,73256,102166,81512,73252,98024,81508,73250,98020,81506,98018,69142,73270,81526,98038,122772,122770,106260,114484,106258,114482,73236,81460,73234,97908,81458,97906,122762,106250,114458,73226,81434,97850,66396,66382,67416,99246,67404,67398,66350,67438,69456,100268,69448,100262,69444,69442,67372,69484,67366,69478,102312,116694,102308,102306,69416,100246,73576,102326,73572,69410,73570,67350,69430,73590,118740,118738,102292,106420,102290,106418,69396,73524,69394,81780,73522,81778,118730,102282,106394,69386,73498,81722,66476,66470,67496,99286,67492,67490,66454,67510,100308,100306,67476,69556,67474,69554,116714]}
+{"_author":"cirakg & claude ai","_license":"Derived from pdf417gen (MIT); see THIRD_PARTY_NOTICES.md and LICENSES/pdf417gen.txt.","_summary":"PDF417 cluster 6 patterns (generated; do not edit).","v":[109536,120312,86976,109040,120060,86496,108792,119934,86256,108668,86136,129744,89056,110072,129736,88560,109820,129732,88312,109694,129730,88188,128464,129772,89592,128456,129766,89340,128452,89214,128450,125904,128492,125896,128486,125892,125890,120784,125932,120776,125926,120772,120770,110544,120812,110536,120806,110532,84928,108016,119548,84448,107768,119422,84208,107644,84088,107582,84028,129640,85488,108284,129636,85240,108158,129634,85116,85054,128232,129654,85756,128228,85630,128226,125416,128246,125412,125410,119784,125430,119780,119778,108520,119798,108516,108514,83424,107256,119166,83184,107132,83064,107070,83004,82974,129588,83704,107390,129586,83580,83518,128116,83838,128114,125172,125170,119284,119282,107508,107506,82672,106876,82552,106814,82492,82462,129562,82812,82750,128058,125050,119034,82296,106686,82236,82206,82366,82108,82078,76736,103920,117500,76256,103672,117374,76016,103548,75896,103486,75836,129384,77296,104188,129380,77048,104062,129378,76924,76862,127720,129398,77564,127716,77438,127714,124392,127734,124388,124386,117736,124406,117732,117730,104424,117750,104420,104418,112096,121592,126334,92608,111856,121468,92384,111736,121406,92272,111676,92216,111646,92188,75232,103160,117118,93664,74992,103036,93424,112252,102974,93304,74812,93244,74782,93214,129332,75512,103294,129908,129330,93944,75388,129906,93820,75326,93758,127604,75646,128756,127602,94078,128754,124148,126452,124146,126450,117236,121844,117234,121842,103412,103410,91584,111344,121212,91360,111224,121150,91248,111164,91192,111134,91164,91150,74480,102780,91888,74360,102718,91768,111422,91708,74270,91678,129306,74620,129850,92028,74558,91966,127546,128634,124026,126202,116986,121338,102906,90848,110968,121022,90736,110908,90680,110878,90652,90638,74104,102590,91000,74044,90940,74014,90910,74174,91070,90480,110780,90424,110750,90396,90382,73916,90556,73886,90526,90296,110686,90268,90254,73822,90334,90204,90190,71136,101112,116094,70896,100988,70776,100926,70716,70686,129204,71416,101246,129202,71292,71230,127348,71550,127346,123636,123634,116212,116210,101364,101362,79296,105200,118140,79072,105080,118078,78960,105020,78904,104990,78876,78862,70384,100732,79600,70264,100670,79480,105278,79420,70174,79390,129178,70524,129466,79740,70462,79678,127290,127866,123514,124666,115962,118266,100858,113376,122232,126654,95424,113264,122172,95328,113208,122142,95280,113180,95256,113166,95244,78560,104824,117950,95968,78448,104764,95856,113468,104734,95800,78364,95772,78350,95758,70008,100542,78712,69948,96120,78652,69918,96060,78622,96030,70078,78782,96190,94912,113008,122044,94816,112952,122014,94768,112924,94744,112910,94732,94726,78192,104636,95088,78136,104606,95032,113054,95004,78094,94990,69820,78268,69790,95164,78238,95134,94560,112824,121950,94512,112796,94488,112782,94476,94470,78008,104542,94648,77980,94620,77966,94606,69726,78046,94686,94384,112732,94360,112718,94348,94342,77916,94428,77902,94414,94296,112686,94284,94278,77870,94318,94252,94246,68336,99708,68216,99646,68156,68126,68476,68414,127162,123258,115450,99834,72416,101752,116414,72304,101692,72248,101662,72220,72206,67960,99518,72568,67900,72508,67870,72478,68030,72638,80576,105840,118460,80480,105784,118430,80432,105756,80408,105742,80396,80390,72048,101564,80752,71992,101534,80696,71964,80668,71950,80654,67772,72124,67742,80828,72094,80798,114016,122552,126814,96832,113968,122524,96800,113944,122510,96784,113932,96776,113926,96772,80224,105656,118366,97120,80176,105628,97072,114076,105614,97048,80140,97036,80134,97030,71864,101470,80312,71836,97208,80284,71822,97180,80270,97166,67678,71902,80350,97246,96576,113840,122460,96544,113816,122446,96528,113804,96520,113798,96516,96514,80048,105564,96688,80024,105550,96664,113870,96652,80006,96646,71772,80092,71758,96732,80078,96718,96416,113752,122414,96400,113740,96392,113734,96388,96386,79960,105518,96472,79948,96460,79942,96454,71726,79982,96494,96336,113708,96328,113702,96324,96322,79916,96364,79910,96358,96296,113686,96292,96290,79894,96310,66936,99006,66876,66846,67006,68976,100028,68920,99998,68892,68878,66748,69052,66718,69022,73056,102072,116574,73008,102044,72984,102030,72972,72966,68792,99934,73144,68764,73116,68750,73102,66654,68830,73182,81216,106160,118620,81184,106136,118606,81168,106124,81160,106118,81156,81154,72880,101980,81328,72856,101966,81304,106190,81292,72838,81286,68700,72924,68686,81372,72910,81358,114336,122712,126894,114320,122700,114312,122694,114308,114306,81056,106072,118574,97696,81040,106060,97680,114380,106054,97672,81028,97668,81026,97666,72792,101934,81112,72780,97752,81100,72774,97740,81094,97734,68654,72814,81134,97774,114256,122668,114248,122662,114244,114242,80976,106028,97488,80968,106022,97480,114278,97476,80962,97474,72748,81004,72742,97516,80998,97510,114216,122646,114212,114210,80936,106006,97384,80932,97380,80930,97378,72726,80950,97398,114196,114194,80916,97332,80914,97330,66236,66206,67256,99166,67228,67214,66142,67294,69296,100188,69272,100174,69260,69254,67164,69340,67150,69326,73376,102232,116654,73360,102220,73352,102214,73348,73346,69208,100142,73432,102254,73420,69190,73414,67118,69230,73454,106320,118700,106312,118694,106308,106306,73296,102188,81616,106348,102182,81608,73284,81604,73282,81602,69164,73324,69158,81644,73318,81638,122792,126934,122788,122786,106280,118678,114536,106276,114532,106274,114530,73256,102166,81512,73252,98024,81508,73250,98020,81506,98018,69142,73270,81526,98038,122772,122770,106260,114484,106258,114482,73236,81460,73234,97908,81458,97906,122762,106250,114458,73226,81434,97850,66396,66382,67416,99246,67404,67398,66350,67438,69456,100268,69448,100262,69444,69442,67372,69484,67366,69478,102312,116694,102308,102306,69416,100246,73576,102326,73572,69410,73570,67350,69430,73590,118740,118738,102292,106420,102290,106418,69396,73524,69394,81780,73522,81778,118730,102282,106394,69386,73498,81722,66476,66470,67496,99286,67492,67490,66454,67510,100308,100306,67476,69556,67474,69554,116714]}
diff --git a/resources/pdf417/ec.json b/resources/pdf417/ec.json
index 19c1a1e..52897e9 100644
--- a/resources/pdf417/ec.json
+++ b/resources/pdf417/ec.json
@@ -1 +1 @@
-{"_author":"cirakg & claude ai","_summary":"PDF417 EC factors per level (generated; do not edit).","v":[[27,917],[522,568,723,809],[237,308,436,284,646,653,428,379],[274,562,232,755,599,524,801,132,295,116,442,428,295,42,176,65],[361,575,922,525,176,586,640,321,536,742,677,742,687,284,193,517,273,494,263,147,593,800,571,320,803,133,231,390,685,330,63,410],[539,422,6,93,862,771,453,106,610,287,107,505,733,877,381,612,723,476,462,172,430,609,858,822,543,376,511,400,672,762,283,184,440,35,519,31,460,594,225,535,517,352,605,158,651,201,488,502,648,733,717,83,404,97,280,771,840,629,4,381,843,623,264,543],[521,310,864,547,858,580,296,379,53,779,897,444,400,925,749,415,822,93,217,208,928,244,583,620,246,148,447,631,292,908,490,704,516,258,457,907,594,723,674,292,272,96,684,432,686,606,860,569,193,219,129,186,236,287,192,775,278,173,40,379,712,463,646,776,171,491,297,763,156,732,95,270,447,90,507,48,228,821,808,898,784,663,627,378,382,262,380,602,754,336,89,614,87,432,670,616,157,374,242,726,600,269,375,898,845,454,354,130,814,587,804,34,211,330,539,297,827,865,37,517,834,315,550,86,801,4,108,539],[524,894,75,766,882,857,74,204,82,586,708,250,905,786,138,720,858,194,311,913,275,190,375,850,438,733,194,280,201,280,828,757,710,814,919,89,68,569,11,204,796,605,540,913,801,700,799,137,439,418,592,668,353,859,370,694,325,240,216,257,284,549,209,884,315,70,329,793,490,274,877,162,749,812,684,461,334,376,849,521,307,291,803,712,19,358,399,908,103,511,51,8,517,225,289,470,637,731,66,255,917,269,463,830,730,433,848,585,136,538,906,90,2,290,743,199,655,903,329,49,802,580,355,588,188,462,10,134,628,320,479,130,739,71,263,318,374,601,192,605,142,673,687,234,722,384,177,752,607,640,455,193,689,707,805,641,48,60,732,621,895,544,261,852,655,309,697,755,756,60,231,773,434,421,726,528,503,118,49,795,32,144,500,238,836,394,280,566,319,9,647,550,73,914,342,126,32,681,331,792,620,60,609,441,180,791,893,754,605,383,228,749,760,213,54,297,134,54,834,299,922,191,910,532,609,829,189,20,167,29,872,449,83,402,41,656,505,579,481,173,404,251,688,95,497,555,642,543,307,159,924,558,648,55,497,10],[352,77,373,504,35,599,428,207,409,574,118,498,285,380,350,492,197,265,920,155,914,299,229,643,294,871,306,88,87,193,352,781,846,75,327,520,435,543,203,666,249,346,781,621,640,268,794,534,539,781,408,390,644,102,476,499,290,632,545,37,858,916,552,41,542,289,122,272,383,800,485,98,752,472,761,107,784,860,658,741,290,204,681,407,855,85,99,62,482,180,20,297,451,593,913,142,808,684,287,536,561,76,653,899,729,567,744,390,513,192,516,258,240,518,794,395,768,848,51,610,384,168,190,826,328,596,786,303,570,381,415,641,156,237,151,429,531,207,676,710,89,168,304,402,40,708,575,162,864,229,65,861,841,512,164,477,221,92,358,785,288,357,850,836,827,736,707,94,8,494,114,521,2,499,851,543,152,729,771,95,248,361,578,323,856,797,289,51,684,466,533,820,669,45,902,452,167,342,244,173,35,463,651,51,699,591,452,578,37,124,298,332,552,43,427,119,662,777,475,850,764,364,578,911,283,711,472,420,245,288,594,394,511,327,589,777,699,688,43,408,842,383,721,521,560,644,714,559,62,145,873,663,713,159,672,729,624,59,193,417,158,209,563,564,343,693,109,608,563,365,181,772,677,310,248,353,708,410,579,870,617,841,632,860,289,536,35,777,618,586,424,833,77,597,346,269,757,632,695,751,331,247,184,45,787,680,18,66,407,369,54,492,228,613,830,922,437,519,644,905,789,420,305,441,207,300,892,827,141,537,381,662,513,56,252,341,242,797,838,837,720,224,307,631,61,87,560,310,756,665,397,808,851,309,473,795,378,31,647,915,459,806,590,731,425,216,548,249,321,881,699,535,673,782,210,815,905,303,843,922,281,73,469,791,660,162,498,308,155,422,907,817,187,62,16,425,535,336,286,437,375,273,610,296,183,923,116,667,751,353,62,366,691,379,687,842,37,357,720,742,330,5,39,923,311,424,242,749,321,54,669,316,342,299,534,105,667,488,640,672,576,540,316,486,721,610,46,656,447,171,616,464,190,531,297,321,762,752,533,175,134,14,381,433,717,45,111,20,596,284,736,138,646,411,877,669,141,919,45,780,407,164,332,899,165,726,600,325,498,655,357,752,768,223,849,647,63,310,863,251,366,304,282,738,675,410,389,244,31,121,303,263]]}
+{"_author":"cirakg & claude ai","_license":"Derived from pdf417gen (MIT); see THIRD_PARTY_NOTICES.md and LICENSES/pdf417gen.txt.","_summary":"PDF417 EC factors per level (generated; do not edit).","v":[[27,917],[522,568,723,809],[237,308,436,284,646,653,428,379],[274,562,232,755,599,524,801,132,295,116,442,428,295,42,176,65],[361,575,922,525,176,586,640,321,536,742,677,742,687,284,193,517,273,494,263,147,593,800,571,320,803,133,231,390,685,330,63,410],[539,422,6,93,862,771,453,106,610,287,107,505,733,877,381,612,723,476,462,172,430,609,858,822,543,376,511,400,672,762,283,184,440,35,519,31,460,594,225,535,517,352,605,158,651,201,488,502,648,733,717,83,404,97,280,771,840,629,4,381,843,623,264,543],[521,310,864,547,858,580,296,379,53,779,897,444,400,925,749,415,822,93,217,208,928,244,583,620,246,148,447,631,292,908,490,704,516,258,457,907,594,723,674,292,272,96,684,432,686,606,860,569,193,219,129,186,236,287,192,775,278,173,40,379,712,463,646,776,171,491,297,763,156,732,95,270,447,90,507,48,228,821,808,898,784,663,627,378,382,262,380,602,754,336,89,614,87,432,670,616,157,374,242,726,600,269,375,898,845,454,354,130,814,587,804,34,211,330,539,297,827,865,37,517,834,315,550,86,801,4,108,539],[524,894,75,766,882,857,74,204,82,586,708,250,905,786,138,720,858,194,311,913,275,190,375,850,438,733,194,280,201,280,828,757,710,814,919,89,68,569,11,204,796,605,540,913,801,700,799,137,439,418,592,668,353,859,370,694,325,240,216,257,284,549,209,884,315,70,329,793,490,274,877,162,749,812,684,461,334,376,849,521,307,291,803,712,19,358,399,908,103,511,51,8,517,225,289,470,637,731,66,255,917,269,463,830,730,433,848,585,136,538,906,90,2,290,743,199,655,903,329,49,802,580,355,588,188,462,10,134,628,320,479,130,739,71,263,318,374,601,192,605,142,673,687,234,722,384,177,752,607,640,455,193,689,707,805,641,48,60,732,621,895,544,261,852,655,309,697,755,756,60,231,773,434,421,726,528,503,118,49,795,32,144,500,238,836,394,280,566,319,9,647,550,73,914,342,126,32,681,331,792,620,60,609,441,180,791,893,754,605,383,228,749,760,213,54,297,134,54,834,299,922,191,910,532,609,829,189,20,167,29,872,449,83,402,41,656,505,579,481,173,404,251,688,95,497,555,642,543,307,159,924,558,648,55,497,10],[352,77,373,504,35,599,428,207,409,574,118,498,285,380,350,492,197,265,920,155,914,299,229,643,294,871,306,88,87,193,352,781,846,75,327,520,435,543,203,666,249,346,781,621,640,268,794,534,539,781,408,390,644,102,476,499,290,632,545,37,858,916,552,41,542,289,122,272,383,800,485,98,752,472,761,107,784,860,658,741,290,204,681,407,855,85,99,62,482,180,20,297,451,593,913,142,808,684,287,536,561,76,653,899,729,567,744,390,513,192,516,258,240,518,794,395,768,848,51,610,384,168,190,826,328,596,786,303,570,381,415,641,156,237,151,429,531,207,676,710,89,168,304,402,40,708,575,162,864,229,65,861,841,512,164,477,221,92,358,785,288,357,850,836,827,736,707,94,8,494,114,521,2,499,851,543,152,729,771,95,248,361,578,323,856,797,289,51,684,466,533,820,669,45,902,452,167,342,244,173,35,463,651,51,699,591,452,578,37,124,298,332,552,43,427,119,662,777,475,850,764,364,578,911,283,711,472,420,245,288,594,394,511,327,589,777,699,688,43,408,842,383,721,521,560,644,714,559,62,145,873,663,713,159,672,729,624,59,193,417,158,209,563,564,343,693,109,608,563,365,181,772,677,310,248,353,708,410,579,870,617,841,632,860,289,536,35,777,618,586,424,833,77,597,346,269,757,632,695,751,331,247,184,45,787,680,18,66,407,369,54,492,228,613,830,922,437,519,644,905,789,420,305,441,207,300,892,827,141,537,381,662,513,56,252,341,242,797,838,837,720,224,307,631,61,87,560,310,756,665,397,808,851,309,473,795,378,31,647,915,459,806,590,731,425,216,548,249,321,881,699,535,673,782,210,815,905,303,843,922,281,73,469,791,660,162,498,308,155,422,907,817,187,62,16,425,535,336,286,437,375,273,610,296,183,923,116,667,751,353,62,366,691,379,687,842,37,357,720,742,330,5,39,923,311,424,242,749,321,54,669,316,342,299,534,105,667,488,640,672,576,540,316,486,721,610,46,656,447,171,616,464,190,531,297,321,762,752,533,175,134,14,381,433,717,45,111,20,596,284,736,138,646,411,877,669,141,919,45,780,407,164,332,899,165,726,600,325,498,655,357,752,768,223,849,647,63,310,863,251,366,304,282,738,675,410,389,244,31,121,303,263]]}
diff --git a/resources/pdf417/text.json b/resources/pdf417/text.json
index e89d01b..36a61f5 100644
--- a/resources/pdf417/text.json
+++ b/resources/pdf417/text.json
@@ -1 +1 @@
-{"_author":"cirakg & claude ai","_summary":"PDF417 text submode values (generated; do not edit).","v":[[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,26,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,26,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1,-1,-1,-1,12,-1,-1,-1,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,26,-1,-1,15,18,21,10,-1,-1,-1,22,20,13,16,17,19,0,1,2,3,4,5,6,7,8,9,14,-1,-1,23,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,24,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1,-1,-1,-1,12,15,-1,-1,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,10,20,-1,18,-1,-1,28,23,24,22,-1,13,16,17,19,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,14,0,1,-1,2,25,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,4,5,6,-1,7,8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,26,21,27,9]]}
+{"_author":"cirakg & claude ai","_license":"Derived from pdf417gen (MIT); see THIRD_PARTY_NOTICES.md and LICENSES/pdf417gen.txt.","_summary":"PDF417 text submode values (generated; do not edit).","v":[[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,26,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,26,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1,-1,-1,-1,12,-1,-1,-1,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,26,-1,-1,15,18,21,10,-1,-1,-1,22,20,13,16,17,19,0,1,2,3,4,5,6,7,8,9,14,-1,-1,23,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,24,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1,-1,-1,-1,12,15,-1,-1,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,10,20,-1,18,-1,-1,28,23,24,22,-1,13,16,17,19,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,14,0,1,-1,2,25,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,4,5,6,-1,7,8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,26,21,27,9]]}
diff --git a/source/codes/QrTables.mc b/source/codes/QrTables.mc
index d88f7b0..b8d5843 100644
--- a/source/codes/QrTables.mc
+++ b/source/codes/QrTables.mc
@@ -1,6 +1,7 @@
// Author: cirakg & claude ai
// Summary: QR error-correction block structure per version and level (generated from segno; do not edit).
//
+// Derived from Segno (BSD-3-Clause). See THIRD_PARTY_NOTICES.md and LICENSES/segno.txt.
// Generated by tools/gen_qr_tables.py. For version v (1-40) and level e (0 L, 1 M, 2 Q, 3 H), the five
// numbers at index ((v - 1) * 4 + e) * 5 are: EC codewords per block, group-1 blocks, data codewords
// per group-1 block, group-2 blocks, data codewords per group-2 block (ISO/IEC 18004 table 9).
diff --git a/tools/gen_pdf417_tables.py b/tools/gen_pdf417_tables.py
index 7a66677..0e63c2d 100644
--- a/tools/gen_pdf417_tables.py
+++ b/tools/gen_pdf417_tables.py
@@ -27,7 +27,10 @@ from pdf417gen import codes, data
SUBMODES = [data.UPPER, data.LOWER, data.MIXED, data.PUNCT]
-HEADER = {"_author": "cirakg & claude ai"}
+HEADER = {
+ "_author": "cirakg & claude ai",
+ "_license": "Derived from pdf417gen (MIT); see THIRD_PARTY_NOTICES.md and LICENSES/pdf417gen.txt.",
+}
def tables() -> dict[str, object]:
diff --git a/tools/gen_qr_tables.py b/tools/gen_qr_tables.py
index 68e6355..64c516a 100644
--- a/tools/gen_qr_tables.py
+++ b/tools/gen_qr_tables.py
@@ -50,6 +50,7 @@ def render(rows: list[list[int]]) -> str:
"// Author: cirakg & claude ai\n"
"// Summary: QR error-correction block structure per version and level (generated from segno; do not edit).\n"
"//\n"
+ "// Derived from Segno (BSD-3-Clause). See THIRD_PARTY_NOTICES.md and LICENSES/segno.txt.\n"
"// Generated by tools/gen_qr_tables.py. For version v (1-40) and level e (0 L, 1 M, 2 Q, 3 H), the five\n"
"// numbers at index ((v - 1) * 4 + e) * 5 are: EC codewords per block, group-1 blocks, data codewords\n"
"// per group-1 block, group-2 blocks, data codewords per group-2 block (ISO/IEC 18004 table 9).\n\n"
diff --git a/tools/lint_project.py b/tools/lint_project.py
index c1c225a..3fe7717 100644
--- a/tools/lint_project.py
+++ b/tools/lint_project.py
@@ -55,6 +55,8 @@ SKIP_DIRS = {
}
# Written by a tool that would drop a hand-added header on the next run.
GENERATED = {Path("docker/docs/package-lock.json")}
+# License texts must retain their upstream wording without project headers.
+VERBATIM_LICENSES = {Path("LICENSE"), Path("LICENSES/segno.txt"), Path("LICENSES/pdf417gen.txt")}
BASE32_SECRET_RE = re.compile(r'"[^"\n]*[A-Z2-7]{16,}[^"\n]*"')
HEX_SECRET_RE = re.compile(r'"[^"\n]*[0-9a-fA-F]{32,}[^"\n]*"')
@@ -141,7 +143,7 @@ def project_files(root: Path) -> list[Path]:
def check_header(path: Path, rel: Path) -> Iterable[Problem]:
"""HDR001 for one file, by file type."""
- if rel in GENERATED:
+ if rel in GENERATED or rel in VERBATIM_LICENSES:
return
suffix = path.suffix.lower()
if suffix == ".png":
diff --git a/tools/tests/test_lint_project.py b/tools/tests/test_lint_project.py
index 31f8125..c38b98f 100644
--- a/tools/tests/test_lint_project.py
+++ b/tools/tests/test_lint_project.py
@@ -106,6 +106,8 @@ def test_headers_are_required_in_every_file_type(tmp_path: Path) -> None:
"data/ok.json": json.dumps({"_author": lp.AUTHOR, "_summary": "x"}),
"data/bad.json": json.dumps({"author": "someone else", "description": "x"}),
"docker/docs/package-lock.json": "{}", # generated, exempt
+ "LICENSE": "Verbatim license text, exempt from project headers.",
+ "LICENSES/segno.txt": "Verbatim upstream license, also exempt.",
"build/out.txt": "ignored folder",
}
for name, text in files.items():