尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Move a secret between two devices by scanning a QR code. Live demo and protocol core.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Move a secret from one device to another by scanning a QR code. One device shows a code, the other scans it, a person checks five digits across the two screens, and only then does the secret travel, sealed, over public relays that neither device runs and that cannot read it.
Live demo: https://sybenx.github.io/qr-secret-transfer/ · Specification · Profiles
Questions or breakage reports: Nostr: npub175rn…sef5g
This repository is three things:
QR_SECRET_TRANSFER.md, with the
profiles that say what kinds of secret it carries (a Nostr
key, a FROST share, or an app's own, such as a sync key for a game) and its
test vectors. The core mechanism knows nothing about what it moves; a
new kind of secret is a new profile, never a change to the specification.docs/, built from src/demo/). It moves a short
piece of text between two devices. It is a static page: no server of its own, no
analytics, nothing loaded from anywhere else, and no network activity until a
transfer is started.src/core/) with no I/O, no DOM and no timers, plus the browser
adapters that run it over relays (src/web/).kotlin/), a port line for line, for
Android and JVM apps. QRST Signer and Cosign build on it.Status: a draft protocol and unaudited code. Use made-up values, not real secrets. The event kinds are provisional and interoperability is not promised.
npm install
npm run build # writes docs/
npm run relay # a local relay on ws://localhost:7777
npm run serve # the page on http://localhost:8080
Open the page in two browser windows (or on two devices that can both reach the
relay). In each, open This device: relays and past transfers and add
ws://localhost:7777. Then choose Receive a secret in one and Send a secret in
the other.
On the public internet no setup is needed: the page finds relays itself (see Relays).
docs/ is the whole site. On GitHub: Settings → Pages → Deploy from a branch →
main / docs. Any static host works; the page uses only relative paths, so it can
live at a domain root or under a path. This repository's copy is live at
https://sybenx.github.io/qr-secret-transfer/.
The page is its own bounce page (spec §11.2a): the QR codes it shows point back at whatever address it is served from, with every parameter in the fragment, so nothing about a pairing ever reaches the host.
Three <meta> tags in src/demo/index.html are the configuration:
| Tag | Meaning |
|---|---|
qrst:relays | Relays you supply, tried first. content="wss://relay.example.com" |
qrst:seeds | Replaces the starting relays this build ships. content="none" ships none. |
qrst:discovery | Relays to ask for NIP-66 discovery events. content="none" asks nobody extra. |
Before publishing a copy elsewhere, set a canonical URL in src/demo/index.html,
and its GitHub links if your copy of the specification lives somewhere else.
The wire protocol of QR_SECRET_TRANSFER.md 1.6-draft:
vectors/qrst-sas.json);nostr-tools in both directions;qrst: handoff to an installed app (§11.2c), which the bounce
page offers and the paste field accepts;1.6-draft moved the wrap from kind 1059 to 21059 because of this implementation: relays treat kind 1059 as a stored direct message and were seen serving one long after its NIP-40 expiry. A 1.5 client, which uses 1059, cannot pair with this one.
The three levels of §9.2 are chosen on each device with a slider, and the stricter of the two devices' settings applies:
| Level | Sender | Receiver | For |
|---|---|---|---|
| Type digits (high) | types the digits shown on the Receiver | shows five digits | what cannot be taken back |
| Compare digits (medium, default) | shows five digits, the user confirms they match | shows five digits | most things |
| Confirm only (low) | consent only; a second responder ends the session | shows nothing | what can be revoked |
Not implemented: the offline tier (§10), the local network path (§11.7, which a
browser cannot do), profile URI schemes such as frost:// (§12.3), reading the code
by camera (§9.2 "capture"), and the published profiles. The demo carries only its
own text profile, qrst-demo-text (src/core/profile.ts).
1.5-draft was written from this implementation: every place 1.4 could be read two
ways, or was wrong, is recorded under "Resolved" in
SPEC_ISSUES.md. To report a new one,
use the specification form;
a bug in this demo or its code is a
plain issue.
No relay is depended on. Before a device shows a code it sends itself a sealed message, padded to the largest payload the profile allows, through each candidate relay. A relay is named in the code only if that message comes back. That test is exactly what a session needs: the relay accepts a gift wrap from a key it has never seen, serves it to a subscriber it has never seen, and carries a payload of that size.
Candidates are tried in this order: relays you or the page configured; relays that
passed on this device before; relays named by a code this device scanned in a session
that completed; relays that NIP-66 monitors report; and last, a short list of seeds
shipped in src/demo/config.ts. A seed is where a device with nothing else starts.
After the first successful session a device prefers what it remembers, and tops that
memory up from discovery in the background about once a day.
QR_SECRET_TRANSFER.md the specification
profiles/ what kinds of secret it carries, one file each
SPEC_ISSUES.md everything settled about the specification so far
src/core/ pure protocol: no I/O, injected randomness and clock
sas.ts the commitment and the five digits (§6)
nip44.ts NIP-44 v2
event.ts Nostr events, BIP-340, npub
wrap.ts seal and gift wrap, and the attribution check (§11.4)
uri.ts the pairing link (§11.2)
profile.ts the profile interface (§5) and the demo's text profile
session.ts the state machine: flows, consent rules, responders (§7, §8, §9, §13)
src/web/ browser adapters
pool.ts relay links, the re-send loop, NIP-42, the loopback test (§11.5)
select.ts relay selection, NIP-11 limits, NIP-66 discovery
store.ts what a device remembers: relays, throttle, transfer log
transfer.ts one transfer end to end, headless
src/demo/ the page
kotlin/ the Kotlin port of src/core and src/web, as a Gradle build
tools/ a local relay with fault injection, a static server, and
payload_ceiling.py, which computes the wire size of a payload
test/ unit, integration and browser tests
vectors/ the specification's test vectors
docs/ the built site
src/core depends on @noble/curves, @noble/hashes, @noble/ciphers and
@scure/base, and nothing else. The page adds qr for drawing and reading codes.
nostr-tools is a development dependency, used only as a second implementation to
test against.
A session is driven by handing it events and user decisions; it answers with effects for the host to carry out (publish this, keep that, write this record). Nothing in it touches a socket or a clock, which is why a whole flow replays byte for byte in a test.
npm run check # typecheck, unit and integration tests, build, browser tests
test/*.test.ts): the §6 vectors including the transposed-roles negative;
the official NIP-44 vectors; gift wraps against nostr-tools; both flows as state
machines, wrong codes, the attempt budget, expiry, multiple responders, planted
payloads, the throttle, burners wiped at the end.test/relays.test.ts, test/transfer.test.ts): real WebSockets against
a local relay that can refuse writes, swallow events, demand authentication, serve
wraps only to their recipient, advertise limits, answer late, or drop every
connection mid-session.test/retransmit.test.ts): each device in turn loses its connection
as the message meant for it goes out, at every step of both flows, over a relay that
keeps nothing and over one that behaves like strfry (keeps ephemeral events five
minutes, refuses a repeated id and anything over a minute old).kotlin/, ./gradlew :qrst:test on Java 21 after npm ci): the same
vectors, the state machine, and both flows in both directions against the
TypeScript code here, over the local relay.test/e2e): two browser contexts as two devices, through both flows;
the page's own camera reading a code from a fake video feed; the release prompt's
rules; relay selection on screen; and that the page asks nothing of the network
until a transfer starts.Two independent reviews of the code against the specification were run before this
was handed over, one of the core and one of the page. They found one serious fault
(the pairing code could be ground: see "The contacting party gets more than one code
per session" in SPEC_ISSUES.md) and about twenty smaller ones. All are
fixed and have regression tests.
The code is MIT (LICENSE). The specification, its profiles and its
test vectors are CC0 (LICENSE-SPEC). The bundled libraries are MIT and the fonts are
under the SIL Open Font License; their notices ship in docs/.