ARMANOS

Two-factor

One-time codes inside the profile

A profile can hold the two-factor key of the account it signs into. The app turns that key into the current code, in the same window, without a phone.
RFC 6238 over RFC 4226
Standard
6 digits, 30 second step
Code
SHA1, SHA256, SHA512
Algorithms
On your machine, no network
Where it is computed

A phone in your hand does not scale to fifty accounts

Two-factor protection is no longer optional. Marketplaces, ad accounts and social platforms all ask for six digits at sign-in, and they ask often.

With two accounts a phone is fine. With fifty it becomes the slowest part of the day. Unlock the phone, find the right entry among fifty that look alike, read the digits, type them before they roll over, repeat.

The secret behind those digits already sits next to the account, in whatever the person keeps accounts in: a spreadsheet, a note, a shared vault. ARMANOS moves it one step closer, into the profile that signs in with it.

This is about the accounts you work with inside profiles. The two-step sign-in on your own ARMANOS account is a separate thing, kept on the server with recovery codes.

  • No second device

    The code appears in the same window where the profile lives.

  • No hunting for the right entry

    The key belongs to the profile, so there is nothing to match by name.

  • No retyping under time pressure

    Copy the code, or let a flow type it into the form.

  • Nothing goes over the network

    The code is computed from the key on this computer.

The key goes in next to the account it belongs to

Open the menu on a profile card and pick One-time code. If the profile has no key yet, the window asks for one instead of showing digits.

Paste either form. The bare key the site prints next to its QR code works, and so does the whole otpauth:// line that the QR code actually contains. Most authenticator exports produce that line.

From the link the app reads the issuer, the account name, the number of digits, the step and the algorithm. A bare key carries none of that, so the app then assumes the common defaults. When the site is unusual, paste the link.

A key that cannot produce codes is refused right there and never stored. Wrong codes fail a login, and a few failed logins lock an account out. An hotp:// link counts sign-ins rather than seconds, so it is refused too.

  1. 1

    Open the profile menu

    One-time code sits in the same menu as Fingerprint and Sign-in details.

  2. 2

    Paste the key

    JBSWY3DPEHPK3PXP or otpauth://totp/... Spaces, dashes, lower case and = padding are all tolerated.

  3. 3

    Save

    Anything that is not usable base32 comes back as an error, and nothing is written to disk.

  4. 4

    Look at the card

    A 2FA mark appears on the profile, so you see at a glance which accounts no longer need a phone.

The code is arithmetic over the clock, done on your machine

The generator follows RFC 6238 over RFC 4226. The current Unix time is divided by the step, which gives a counter. The counter is hashed with your key using HMAC, and the digits are cut out of that hash.

The counter is written as two 32-bit halves. A single 32-bit write is silently wrong once the counter grows past about four billion, so the test pins that case with the published vector at t=20000000000.

Nothing is sent anywhere. The key stays in the file, the hashing happens in the app, and the answer comes back with the seconds left in the current window. Because of that the code also works with no internet at all.

SettingDefaultWhat is accepted
Digits66, 7 or 8, taken from an otpauth link
Step30 seconds1 to 300 seconds, taken from the link
AlgorithmHMAC-SHA1SHA1, SHA256 and SHA512
Key formatbase32Spaces, dashes, lower case and = padding

The window shows the code and how long it has left

The code sits large in the middle, grouped three and three. On screen, 482 913 is easier to carry across to a form than 482913, and the copy button puts it on the clipboard without the space.

A bar under the code drains as the window closes, and the value refreshes every second. The last five seconds are marked, so nobody starts typing a code that is about to roll over.

The line above names the issuer and the account read from the otpauth link. That is what tells you the code belongs to the account you think it does, when a profile is one of fifty.

Replace the key swaps it for a new one, which is what you do after re-enrolling an account. Remove asks for confirmation first, then the profile stops producing codes.

A script asks for the code at the second it needs it

The desktop app carries a local HTTP API for programs on the same computer. It is off until you switch it on, it listens on 127.0.0.1 only, and every call carries a token.

GET /api/v1/profile/code takes a profileId and answers with the current code and the seconds remaining. That is the point of doing this over an API: a script that signs in needs the second factor at the moment the form asks, not thirty seconds earlier.

Requests that look like they came from a web page are refused. An Origin header, a Sec-Fetch-Site other than none, or a Host that is not the loopback address all end the call. A page you happen to visit must never be able to read this.

An AI assistant reaches the same route through the bundled MCP server, as the tool get_one_time_code. It goes through the same token and the same refusals.

Call
GET /api/v1/profile/code?profileId=p-42
Token
token parameter, x-api-token header or Authorization: Bearer
Answer
{ "code": 0, "data": { "code": "482913", "secondsRemaining": 17, "issuer": "Shop" } }
Profile has no key
code -1, msg no_2fa_key
No token or a wrong one
HTTP 401
Sent from a web page
Refused, whatever the token says

The 0 is the envelope status shared by every route. The six digits are in data.code.

The Local automation API screen in ARMANOS: an Enable local API switch turned on and marked Running, the address http://127.0.0.1:50327, an access token with a Copy button, and an MCP server card below.
The API is off until you turn it on. The token from this screen is what a script sends with every call, including the one that asks for a code.

A flow types the code itself

In the flow builder any text field accepts {{profile.totp}}. Put it in an input step aimed at the code field on the login page, and the flow fills it in like the rest of the form.

The variable is a function, not a stored value. The variable map for a run is built once, and a code written into it would already be stale by the third step. It is computed at the moment of substitution instead.

The run log never prints it. An input step whose value came from a variable is written to the log as four dots, because that log lands on disk and is shown in the window.

The step focuses the field, clears it, types the value and fires the input and change events, so a login form that watches for real typing still reacts.

The key stays on this computer

The key lives in profiles.json in the app data folder, next to the rest of the profile. It is written atomically: to a neighbour file, then flushed to disk, then renamed. A power cut leaves either the old file whole or the new one whole.

The file is written with permissions 600, owner only, and files left open by older versions are closed at startup. Those permissions travel with the file when it is copied into a backup or a synced folder.

On Windows chmod only touches the read-only flag, so those permissions do nothing there. What protects the file is the user profile folder, which other accounts on the machine do not enter without administrator rights.

Where the key does and does not travel is decided per action. An export asks first and strips secrets unless you say to include them. A duplicate never takes the key, because a copy is for a different account. Signing in on a second machine keeps it, since the server has no column for it. The transfer bundle carries it under your password, and the text before that password names it. The problem report cuts it out, and the change history records that the key changed, never its value.

The key is not encrypted on disk

Account passwords and proxy passwords are sealed with the system keychain. The 2FA key is not: it lies in profiles.json as text, closed to other users of the machine by file permissions. Treat the computer itself as the thing that protects it.

Fifty keys arrive from a spreadsheet

The spreadsheet import recognises a 2FA column by its name. It accepts 2fa, totp, otp, authenticator, secretkey and the Russian spellings, so an existing table usually needs no renaming.

Nothing is created until you have seen which column became which field. Rows whose key cannot produce codes are marked in the preview, and one checkbox skips them instead of creating profiles with a broken key.

Restoring a profile export follows the same care. The digits, the step and the algorithm travel with the key. Reading back only the bare secret would quietly reset an 8-digit, 60-second key to the defaults, and the account would simply stop letting you in.

There is no bulk action that attaches a key to profiles you already have. Keys arrive either one at a time in the profile window, or as a column when the profiles are created.

What this does not do

  • It is not a password manager. One key per profile, no vault, no search across keys, and nowhere to put the backup codes a site hands you.
  • The key is not encrypted on disk. File permissions close it to other users on macOS and Linux; on Windows only the user profile folder does that.
  • A lost key is lost. It never reaches the server, and nothing in the app can rebuild it from the codes it has produced.
  • No pictures are read. You paste the key text or the otpauth:// line; the app does not scan a QR image or a phone screen.
  • The code follows this computer's clock. If the clock drifts, sites refuse the code, and the app does not warn you that this is why.

How to check

Every claim above has a file behind it, and most have a stand that fails when the claim stops being true.

The codes match the published RFC 6238 vectors, including the counter past 32 bits
apps/desktop/src/lib/totp.js · apps/desktop/test/totp.js
The file holding the key is written owner-only, and files left open by older versions are closed at startup
apps/desktop/src/lib/secureFile.js · apps/desktop/test/secrets-on-disk.js
The code route answers only with the token, and never a web page
apps/desktop/src/lib/local-api.js · apps/desktop/test/local-api-gate.js
A flow computes the code at the moment it substitutes it
apps/desktop/src/main/main.js · apps/web/test/страница-возможностей.js
A restored profile keeps its digits, step and algorithm
apps/desktop/src/main/main.js · apps/desktop/test/config-import-2fa.js

Put the key where the account lives

Install ARMANOS, add a key to one profile, and see the code appear in the same window.