Repository navigation
Import authenticator codes into any key that can store them (re #170) - #173
Merged
Merged
Conversation
The parsers feeding the Molto2 slot programmer are about to gain wider siblings for keys that store HOTP, other TOTP periods and SHA-512. Pin what the Molto2 path accepts, skips and reports today before anything moves: the exact Debug of every accepted entry and the exact text of every error, across Aegis, 2FAS, otpauth lists, parse_any, Google Authenticator payloads and single otpauth:// URIs. Any refactor that changes a Molto2 import result now fails here first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…; Molto2 path unchanged (re #170) OATH keys and Token2 OTP storage can hold entries a Molto2 slot can't: HOTP with its counter, TOTP periods other than 30/60 s, SHA-512, other digit counts and longer seeds. The existing parsers drop or reject those before any key is chosen, and stop a whole file at the first entry the Molto2 can't take. Add a format-neutral ImportEntry/ImportFile model and wide parsers for otpauth:// URIs, Aegis, 2FAS, otpauth lists (now also accepting Google Authenticator migration lines), migration payloads and QR screenshots. Every entry in a file becomes an item, in file order, either usable or carrying a plain reason (Steam/mOTP/Yandex, MD5, a bad secret, ...); only bad JSON, an encrypted export, or an empty file fail the whole file. Per-key limits are applied later, against the chosen destination. The narrow Molto2 parsers keep their own mapping and error order; the JSON structs are shared, and the new counter fields read leniently so a file the Molto2 path accepted can't start failing. The golden tests from the previous commit pass unchanged. Seeds stay in wipe-on-drop buffers and Debug shows only their length. The wide parsers join the fuzz targets. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…rough one entry point (re #170) The import flow starts from a file the person drops or picks, and that file may be a certificate, a key or something else entirely. Guessing a destination from such a file would be wrong, and the CLI and GUI each sniffing formats on their own would drift apart. classify() sorts a file by content (and, for binary DER and OpenPGP, by extension) into an authenticator export, recognized key material, or unknown, without decrypting or decoding anything and without its JSON probes materializing a seed. Key material comes with a plain pointer to where keyroost imports it (PIV certificates, OpenPGP RSA keys) or that it doesn't, each ending "Nothing was changed." open_file() in keyroost-qr is the single reader both front ends will use: it refuses files over 16 MiB, asks for the Aegis password when a vault needs one, decrypts into a wipe-on-drop buffer, reads QR screenshots, and returns the wide entry model. keyroost-qr now enables keyroost-import's existing `encrypted` feature for the vault path; no new crates. Both functions join the fuzz targets. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
#170) The wide readers turn every line or account into an item. A 16 MiB file of junk lines after one otpauth line, or a Google Authenticator payload of empty accounts, could therefore become millions of items and gigabytes of memory. Every wide reader (lists, migration payloads, QR images, Aegis and 2FAS JSON) now refuses a file past MAX_ENTRIES (1000, far above any real export) as a whole, before building more items. The JSON paths count entries without materializing them first. Review follow-ups: - An encrypted Aegis vault is recognized only with Aegis's full header shape, so a config with a string "db" isn't taken for a vault. - File-reading errors say what is wrong and where (line and column, or a missing field's schema name) and never quote values from the file. Unusual type or hash names aren't echoed. - A wrong vault password is a typed error. Its text is unchanged, so CLI output stays the same. - open_path/read_bounded enforce the 16 MiB cap while reading, so an oversized or endless source never lands in memory. - A checked-in encrypted vault fixture, reproducible from the keyroost-import test that documents it, covers decryption end to end through open_file. - The Molto2 path again reads exactly the fields it always did, so a repeated "counter" key no longer fails a file it used to accept. - The golden suite now fingerprints decoded seed bytes (hashed, never printed) and adds query-string edge cases. It refuses to build without its feature and passes unchanged against the pre-series code. - Both otpauth parsers share one URI splitter and query tokenizer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…#170) The Molto2 path never reads an entry's counter, so a TOTP entry with a repeated or non-numeric counter imports there. The wide readers refused the same entry (a repeated JSON key failed the whole file; a bad counter= on a TOTP URI marked it unusable), so the import plan's Molto2 rules could not accept exactly what the Molto2 path accepts. The Aegis and 2FAS wide readers now read the Molto2 path's own fields first and each entry's counter in a separate pass that skips every other key unread. A repeated counter only makes a HOTP entry unusable, and a bad counter= on a URI only matters for HOTP. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…t gets skipped (re #170) Before anything is written, each entry of a file gets one verdict for the chosen key: add, replace, already there, or skipped with a plain reason that names the device ("Solo 2 doesn't take SHA-512 codes"). Nothing is altered to fit. One rule table holds every per-key limit, each citing the research: - YubiKey: 64-byte whole name, SHA-512 from 4.3.1, 32 codes (64 from 5.7); - Solo 2: 127-byte name (a 128-byte one makes the key stop responding), 64-byte secret, no SHA-512; - Nitrokey 3: 128-byte name and secret, 50 codes, 30-second TOTP only (the period isn't stored), no SHA-512; - other OATH keys: only 6-8 digits and the one-command size up front; - Token2 OTP: ASCII app/account up to 64 bytes, 4-10 digits, periods 1-65535, 50 codes, and HOTP only with a starting counter of 0; - Molto2: its existing rules and slot model. Limits the research couldn't confirm aren't enforced; the key's own refusal is reported for that entry instead. Names already on the key are "already there" unless replacing was asked for; replacing doesn't use room, and entries past the known capacity are skipped as "no room". A test checks the Molto2 rules accept exactly what the Molto2 import path writes, entry by entry, over the golden fixtures. The fuzz target now plans every parsed file for every key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…he first error (re #170) An import is many independent writes, so an error halfway leaves the earlier codes on the key. The write loop makes that honest: it writes the plan's add and replace rows in file order, stops at the first error, and reports each row as added, replaced, already there, failed, not tried, or no room left once the key says it is full. A code that already works is never removed on a plain add. The old entry is deleted only after the key refuses an add as a duplicate and replacing was asked for; if the add after that fails, the report says the old entry was removed but the new one could not be added. A Molto2 slot write is never preceded by a delete. The key's adapters (next) map its answers to "exists", "full" or a plain failure. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…eports after review (re #170) - The plan and the Molto2 writer now share one title rule, and the guard test compares the exact title, so they can't drift apart silently. - A 30-second TOTP name that starts with digits and '/' (an issuer like "2024/Corp" as well as a bare account) is skipped as ambiguous, because authenticator apps would read it as a period. With a period prefix it reads back whole and is added. Nitrokey 3 keeps no period in the name, so nothing is skipped there. - A test pins that a replace on a full key isn't skipped for room. - The 2FAS counter pass reads each service's counter without the seed, so no second copy of any seed is made. - When a replace removes the old entry and the new one can't be added, the stop reason names the entry and says why. - The Token2 reason says "plain ASCII characters", and the Solo 2 secret cap explains why it stays at 64 bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…from the key's answer (re #170) A bulk import has to tell "this name is already on the key" apart from "the key is full" and "the applet wants its password again", and it has to do that from the status word the key returns, not from a guess made before sending. The OATH adapter maps PUT 6983 to "exists" (applets that refuse duplicates), 6A84 to "full" and 6982 to "needs the password"; DELETE never reads 6983 as "exists", so a replace can't loop. Applets that drop authorization after each command need a fresh VALIDATE before the next write. The session now keeps the access key from the last successful unlock (wiped on drop) so it can re-SELECT and VALIDATE once without asking for the password again; a second refusal is reported instead of retried. The adapter runs over a small I/O trait, so the rules are tested against a scripted applet without PC/SC, and the plan's command-fit rule is cross-checked against the real PUT builder. keyroost-transport gains an in-tree edge to keyroost-import (default features, no serde). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…cked" on Token2 (re #170) The bulk import writes to Token2 OTP storage and Molto2 slots through the same one-code-at-a-time store the OATH applets use. On Token2 keys 6983 means the OTP PIN is blocked, so it is reported as that and never read as "the name exists" the way an OATH applet's 6983 is; a duplicate name there is simply overwritten by the write. When the key closes its PIN window mid-run (6982/6985) and a PIN is held, it is verified again and the write retried once; with no PIN the key's request is reported instead of guessed at. Token2 entries keep the field values `otp add` uses (the period as the time step, 30 for HOTP, no button press). Molto2 slots get seed, title and config in the order the existing import uses. Anything the plan already keeps out (HOTP, other periods, SHA-512, a seed or title past the command limits) fails for that entry instead of reaching a builder that would panic on it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…bytes in oath add (re #170) A TOTP stored with a non-30 s period carries it as a `period/` name prefix (the ykman convention, e.g. `60/GitHub:alice`), and the bulk import writes names that way. `oath code` and the GUI still computed every code at 30 s, so a 60 s entry showed the wrong code. The OATH byte layer now splits a stored name into period, issuer and account; `oath code` uses the prefix unless --period is given (and refuses 0, which used to reach a panic), and the GUI computes each row and runs its countdown ring on the row's own period. A Solo 2 name longer than 127 bytes makes the key stop responding until it is unplugged, so `oath add` and the GUI add dialog refuse it before anything is sent: the CLI checks right after choosing the key and before reading the seed or password. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…clearer refusals after review (re #170) The Molto2 store wrote the time it was created into every slot's config, and that field also sets the token's clock, so a long run left the clock behind by the run's length. It now reads the clock just before each config goes out, as the single-slot writes do; the clock is injectable so tests can pin it. The Token2 store re-verified the OTP PIN whenever the key answered 6982/6985, which could spend an attempt on a wrong PIN. It now re-verifies only a PIN that already worked in this import, at most once per write, and a wrong PIN is reported plainly with the tries left when the key says. An OATH applet that asks for a password nobody gave now says the storage is password-protected and how to give one (the front end supplies that part) instead of claiming the password was refused. The Solo 2 name refusal lives in one place, OathFamily::name_refusal, shared by `oath add` and the GUI add dialog; the reauthenticate doc marks the Solo 2 behavior as read from the firmware source, not verified on hardware. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…place, --dry-run (re #170) An authenticator export can now go straight to a key's OATH or Token2 OTP storage instead of one `add` per code. The key is read before anything is asked, so the person sees what each entry becomes (added, replacing, already there, or skipped with the reason the key can't store it) and confirms once. Codes already on the key are left alone unless --replace, which is the one word for replacing entries on a key; --overwrite keeps meaning "replace a local output file". The key is read again after the question: if its codes changed while the person was answering, nothing is written. Writing stops at the first error and the closing line says where, what was added before that and how many weren't tried, with exit 1; skipped entries are not failures. The file is read and sorted before any key is looked at, so a certificate or private key gets a pointer to where it belongs and a missing file fails without touching a key. The export's password is --file-password (stdin line 1), the key's own secret keeps its usual flag (--password for OATH, --pin for Token2 OTP; line 2). The preview, summary and --json output name codes only; no seed ever appears in them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
… (re #170) `molto import`, `oath import` and `otp import` now name the same job the same way: the password of an encrypted export is --file-password on all three, so --password on an import always means the key's own secret. The molto flag was added unreleased in this cycle, so no row is kept for it. The v0.12 spellings --password-env / --password-stdin on `molto import` now point at --file-password; the generic rows would have named --password, which `molto import` no longer has. The retired `import-file` note names it too, and the library's "Aegis export is encrypted" message (the golden test pins it) names the new flag. Everything else `molto import` prints is unchanged: it keeps its own file reader, slot numbering and messages. The one other difference is a binary file that is recognized key material (a DER certificate, a PKCS #12 bundle): it now gets a pointer to where it belongs instead of "neither a text export nor a PNG/JPEG image". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…e (re #170) The line-order test credited `oath import` and `otp import` through the generic pair reader, which their handlers never call, so a change that sent the file password to the key as its PIN would have stayed green. The handlers now take the command's own secret pair and read it through two functions, one for the file and one for the key's password or PIN, and the test drives exactly those: both secrets piped (line 1 opens an encrypted export, or is read and dropped for a plain one, and line 2 is the key's secret) and each one alone on line 1. The import JSON gets the same key-shape test as every other --json output. `molto import --file-password` help says "file", and the warning for a password given to a file that isn't encrypted is now one shared sentence (the one `molto import` has always printed). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…re anything opens Only the first dropped file was used, and it went straight into the Molto2-only bulk dialog whatever it was. Now every dropped file joins a queue (each path once) and is read on the import thread one at a time, only when no dialog, notice or device job is running, so a drop during an operation waits for it. An authenticator export opens the import dialog (asking for the file password inside it when needed); a certificate or other key material gets a short notice saying where it belongs, and an unreadable file says so; nothing else happens. "Import file..." in the Authenticator, OTP and Molto2 views picks a file into the same queue and replaces the Molto2 "Bulk import..." button. re #170 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…s codes; replaces the Molto2 bulk dialog The bulk dialog could only fill Molto2 slots, so a YubiKey, Solo 2, Nitrokey 3 or Token2 key got nothing from an export. The new dialog is driven by the file: it shows the entries first (asking for an encrypted vault's password inside), then every connected key that can store codes, a key with both OATH and OTP storage twice, each with how many entries its rules accept. The viewed key (or the store of the open tab, when it has two), else the only capable key, is preselected; otherwise nothing is, and the reason line says to choose. The list follows hotplug, and a key that goes away takes its preview and typed password with it. The preview reads what the chosen key holds and shows per entry what happens (add, replace, already on the key, skipped with the reason); same-name entries are skipped unless "Replace existing entries with the same name" is ticked. The key's password or PIN is asked for in the dialog, or reused from its pane. "Add N codes to <key>" re-reads the key and refuses if its codes changed, then writes one at a time through the shared write loop, stopping on the first error (Molto2 too), and shows the report. Nothing is held open while the person reads or types. For the Molto2 the #171 behavior carries over: Close and [X] settle after the window is drawn, the amber reason line keeps its wording, and Authenticate appears when the token isn't unlocked. Every close route wipes the file password and the key password and drops the parsed entries. The OATH list now shows a period-prefixed name such as 60/GitHub:alice as "GitHub" with its period, rather than "60/GitHub". re #170 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
Closing the import dialog while an encrypted vault was still being decrypted let the late result open a fresh dialog with the decrypted entries (or a notice with the wrong-password error). A retry's result now only lands in the dialog that asked for it; otherwise it is dropped, and the entries with it. Also after review: a refused OATH password during Add wipes the field and asks again, as the preview already did; masked fields have their undo history scrubbed when they are wiped on a refusal, a destination change or a successful open; an unlocked Molto2 whose slot sweep failed says to refresh the slots instead of "read when unlocked"; a Token2 key supplied without the OTP function says so plainly, as the OTP pane and the CLI do; and the result routing (dialog, notice, dropped) is tested. re #170 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
… (re #170) A Solo 2 with an OATH password keeps its unlock for one command only. Checked on a test key: the import's first PUT after the listing came back 6985 ("conditions of use not satisfied"), not 6982, so the import never re-unlocked and stopped at the first entry with nothing written. Both answers now mean "unlock again and retry once"; a key that still refuses is reported as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
The import preview treated a slot as in use only when it had a title, so a slot programmed without one (or whose title was cleared) was planned as a plain add and written over without the "replace" row and note. `molto import` and the Molto2 slot list both go by the seed; the preview now counts a slot as in use when it has a seed or a title. re #170 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
ykman refuses to add a name that a different name already on the key starts with on firmware after 4.0.0 and before 4.3.5, because of a known firmware issue when overwriting in that case. The import plan only compared whole names, so such an entry was planned as a plain add, and single `oath add` sent it too. One shared check, `OathFamily::prefix_refusal`, mirrors ykman's rule; the import plan skips the entry with a plain reason (names planned earlier in the file count as on the key), and `oath add` in the CLI and the GUI refuses before anything is written. The direction is ykman's: a longer name next to a shorter one on the key is still allowed. re #170 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
… alone
The CLI and the GUI each had their own copy of the format and hash names,
the preview and report wording, the summary line and the re-check, and
the copies had drifted ("Aegis, encrypted" vs "Aegis (encrypted)"; one
meaning called "exists", "left as is", "already there" and "already on
the key"). They now come from keyroost-import: `Format::name`,
`HashAlg::name`, `Verdict::word`/`reason`, `Outcome::word`/`reason`,
`Report::summary`, `PlanRow::shown_label`, `Plan::same_rows` and
`plan_for`. One word per meaning: entries are in the file, codes are on
the key; add / replace / exists / skip; "left as is"; nothing says
"overwrite" or "written over". The JSON outcome "already-there" is now
"exists" (unreleased). The Nitrokey 3 touch note says the key may ask
for a touch, since that depends on its firmware. The CLI progress line
says "replacing" for a replace.
Report rows keep a Molto2 slot number (`#5 GitHub — added`), so after
a stop the person can see which slot failed.
In the GUI import dialog:
- choosing a Molto2 destination selects that Molto2 (as clicking it in
the list would), so Authenticate and Add work from the dialog alone;
the reason lines no longer send the person to the list or the Molto2
view for the default key;
- a Molto2 import that wrote everything shows its report like every
other destination instead of closing silently, and the slot list is
read again if the sweep after the writes failed;
- "Add" never does nothing silently: when it can't start, the dialog
says why.
re #170
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
The bounded file read grew a plain Vec with read_to_end and only the final buffer was wrapped for wiping, so every reallocation freed a copy of a plaintext export (every seed in clear) without wiping it. `read_bounded` now reads in chunks into a wipe-on-drop buffer that grows by copying into a new wipe-on-drop buffer, and returns it as such; the CLI and the GUI keep it that way. The password and open messages for an encrypted Aegis file now say "export", like the rest of the import flow, instead of "vault". re #170 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
The OATH import adapter sent whatever name the plan gave it. Every caller plans with the right family today, but a caller that planned with the generic family (a Solo 2 whose vendor string isn't recognized) would send a name that makes the key stop responding. `OathImportStore::new` now takes the family and refuses a name past its hard limit before sending, as a second line of defense behind the plan. Token2: the store started out treating the PIN as unproven although the read just before had verified it on the same session, so a PIN window the key closed before the first write was reported as "the key didn't accept the OTP PIN". A PIN verified on the session now counts as proven (the window is reopened with it, as for later writes), and the unproven case says plainly that the window closed and no attempt was used. re #170 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
Skip reasons, plan notes, store errors, unusable-entry reasons, export formats, key-material kinds and OATH families are all lists that grow as devices and formats are added. Marking them `#[non_exhaustive]` now, while they are new, keeps a later addition from being a breaking change for other crates. Store, Verdict, Outcome, AuthFormat and FileKind stay exhaustive on purpose: the front ends and the file reader must handle every case, and the compiler should say so when one is added. re #170 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
rustdoc read "[S1][S6]" in the new prefix-rule doc comment as intra-doc links and failed with -D warnings. re #170 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
Three edges from the re-review of the bulk-import fixes: - Token2: the import store counted any open PIN window as proof of the typed PIN, but a fingerprint unlock opens the same window without checking a PIN, so a later closed window could re-send an unchecked (possibly wrong) PIN and spend an attempt. The session now tracks a typed-PIN verify separately, and only that lets the store re-send it; otherwise the closed window is reported plainly with nothing sent. - YubiKey 4 firmware 4.0-4.3.4: an entry whose exact name is already on the key is "exists" (left as is) before the prefix rule is looked at, since nothing is written; with replace it is still refused, as ykman does. - GUI: a Molto2 selection the dialog is waiting to make (while a device job runs) is dropped when the person selects another key meanwhile, or the destination changes, or the dialog closes, so it never moves the selection back against a later click. re #170 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
A Molto2 row carries no serial of its own: enumeration never contacts the token, so `-d serial:X` compared against an empty string and every serial form failed, while `list` shows the USB serial in the reader name and `molto info` and the GUI show the card serial. Only the list number worked. `--device` now also matches a Molto2 by the serial in its reader name (no card contact), and, only when the value matched nothing else, by the card serial read from each connected Molto2 in a short session. Other keys never wait on a Molto2, and a value that matches cheaply never touches the token. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
`molto import --in` kept its own reader and writer: one entry the Molto2 can't store refused the whole file, an entry with no name still took a slot, and its dry run listed raw entry fields. The GUI's Molto2 destination already went through the shared plan, so the same file did different things in the CLI and the GUI. The file mode now uses the shared reader, plan, preview, single question, progress lines, report, exit codes and JSON that `oath import` and `otp import` use, with the Molto2 slot model: entries fill consecutive slots from --slot, slots in use are replaced and the preview says so per row, and entries the Molto2 can't store are skipped with the reason. The dry run reads slot use from a connected Molto2 (no customer key needed) and still works with none connected, saying slot use wasn't read. Slot use is read by one shared function in the transport adapter, which the GUI now calls too. Stdin keeps the customer key on line 1 and the export's password on line 2. The single --uri / --qr mode is unchanged. re #170 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
…ilent token Review follow-ups for the shared Molto2 import and the serial lookup: - the CLI and the GUI now call one `molto_plan`, and a test checks that both ways of reading slot use give the same plan for the same file and start slot; - a dry run whose Molto2 doesn't answer shows the preview without slot use and says why, instead of failing (a real import still stops); - the preview header names the start slot it is given rather than working it back from the plan's capacity; - the card-serial lookup runs only for commands that can act on a Molto2, and a Molto2 that doesn't answer is named in a note rather than left out silently; - refusals and `list --json` offer `serial:<reader serial>` for a Molto2, which stays put when keys come and go; - test helper reuse and a rewrapped module comment. re #170 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
The import now reaches OATH keys and Token2 OTP storage, and molto import follows the same rules, so a person needs one place that says what each key can hold, why an entry is skipped, and how --replace and the export's password work. The README gains an "Importing codes" section with the per-key limits; the OATH, OTP and Molto2 Learn pages show the GUI dialog and CLI; the migration page lists what changed for molto import, the Molto2 dialog and oath code --period; changelog fragments cover each change, and the #171 fragment now names the dialog that ships. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
#170) molto import already skips what doesn't fit before slot #99 and writes the rest; the GUI refused the whole import with "Only F of C entries fit". Both now use the shared Molto2 plan's skip ("no slot left after #99"), so the same file plans the same rows in either front end. The blocker, its slot count and its message go; the parity test covers an overflowing file, and the README, Molto2 page and #171 fragment no longer describe it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
Choosing a Molto2 by serial and the molto import dry run's --device refusal never shipped: v0.12.0 took no serials and ignored --device on a dry run. The serial note moves into the #165 key-selection entry and its "fixed" fragment goes; the migration page drops the matching row and says the old dry run ignored --device. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
… (re #170) The serial/clock block is the "which token, and is its clock off" check every Molto2 write prints before it changes anything. The new import reads slot use through the same helper, so a dry run printed it too, although it writes nothing; `oath import --dry-run` shows only the preview. A real import still prints it before the question. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
With no key that can store codes connected, the dialog showed the "Connect a key that can store authenticator codes…" sentence twice: under "Add to" and again as the reason line under the buttons. The list now says briefly that no connected key can store codes; the reason line keeps the instruction. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU
Contributor
|
Token2 on device OTP can be password protected starting from R3.4 - has this been taken into account? |
Owner
Author
|
The R3.4 OTP PIN is covered: if the key has one, the import asks for it once, uses it to find codes already on the key, and never spends an extra attempt. Apologies in advance if I misunderstood the question. |
Contributor
Great, looking forward |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Until now only the Molto2 could take a whole authenticator export at once. This adds the same import for every key keyroost manages that stores codes, from the CLI and the GUI.
What a person gets
keyroostctl oath import --in FILE(YubiKey, Solo 2, Nitrokey 3) andkeyroostctl otp import --in FILE(Token2 OTP storage).molto importnow works the same way.--file-password), 2FAS, otpauth:// lists, QR screenshots including Google Authenticator exports.--replace(GUI: "Replace codes already on the key that have the same name"). Re-running an import is safe.--dry-runshows the preview and writes nothing.Per-key limits (from the vendors' published specs and source; checked before writing)
oath add); no SHA-512.Also in this PR
60/…(or anyperiod/prefix) are now computed with that period inoath codeand the GUI; they were computed at 30 s.oath code --period 0is refused instead of crashing.-dselects a Molto2 by the seriallistormolto infoshows.molto import --passwordis now--file-password, the same word as the other imports.Testing
--replace, password-protected keys, generated codes checked against the expected values (30, 45 and 60 s); Molto2 — CLI import and the GUI dialog. Token2 OTP storage and Nitrokey 3 import are built from the published specs; reports from people with those keys are welcome.🤖 Generated with Claude Code
https://claude.ai/code/session_01Skvxkb6eMzytjPDmL6mPYU