Two-factor
One-time codes inside the profile
- 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
Open the profile menu
One-time code sits in the same menu as Fingerprint and Sign-in details.
- 2
Paste the key
JBSWY3DPEHPK3PXP or otpauth://totp/... Spaces, dashes, lower case and = padding are all tolerated.
- 3
Save
Anything that is not usable base32 comes back as an error, and nothing is written to disk.
- 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.
| Setting | Default | What is accepted |
|---|---|---|
| Digits | 6 | 6, 7 or 8, taken from an otpauth link |
| Step | 30 seconds | 1 to 300 seconds, taken from the link |
| Algorithm | HMAC-SHA1 | SHA1, SHA256 and SHA512 |
| Key format | base32 | Spaces, 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.

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
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
Read next
Profiles
Each account gets its own browser: cookies, storage, fingerprint and proxy.
Local API
The route that hands a script the code, and the token that guards it.
Flow builder
Where {{profile.totp}} goes into the login form.
Import from a spreadsheet
Bring names, proxies and 2FA keys in from a table you already have.
Put the key where the account lives
Install ARMANOS, add a key to one profile, and see the code appear in the same window.