An WIP, no longer active unofficial JavaScript port of magic-wormhole. I suggest checking out magic-wormhole.rs's WASM output instead. Or if you just want something you can run with npx, see magic-wormhole-npm.
This gets as far as establishing a secure channel ("Wormohle") between the two parties and sending a simple text message, but does not yet implement the remainder of the file transfer protocol.
npm install
to install dependencies, then
node cli.js send-demo
will send the text "example" using a trivial password and
node cli.js receive 0-wormhole-code
will receive text.
These interoperate with the python implementation, so you can receive text sent by send-demo
using wormhole receive --only-text 0-wormhole-code
and send text to be received by receive
using wormhole send --text example
.
Spake2 is not widely implemented. This project uses a rust implementation compiled to WebAssembly.
To build the .wasm
you must have rust
and wasm-pack
installed. The built artifacts are committed to this repository, in case you don't have the requisite build tools. The readme under [spake2-wasm][spake2-wasm/] contains notes about building it.
You will need a version of node
which supports WebAssembly to use this project.
I find the easiest way to think about the magic-wormhole protocol is as a series of nested protocols. They are documented below.
Given a rendezvous server (by default ws://relay.magic-wormhole.io:4000/v1
) and an appId
(by default lothar.com/wormhole/text-or-file-xfer
),
The overall magic-wormhole protocol for the sender consists of:
- Open a websocket to the rendezvous server.
- Establish a channel using the rendezvous protocol on top of the websocket.
- Pick a short hex string at random to be
side
. - Establish a channel using the unencrypted protocol on top of the channel using the rendezvous protocol using
appId
andside
. - Pick an ephemeral password at random.
- Let
nameplate
be the nameplate string from the unencrypted channel. - Communicate the
nameplate
and the ephemeral password to the receiver. - Let
password
be the concatenation ofnameplate
,-
, and the ephemeral password. - Establish a channel using the encrypted protocol on top of the channel using the unencrypted protocol using
appId
,side
, andpassword
. - Send
("version", Utf8Encode(JsonStringify({"app_version":{}})))
over the channel using the encrypted protocol. - Receive a message
theirEncodedVersion
over the channel using the encrypted protocol. - Assert:
JsonParse(Utf8Decode(theirEncodedVersion))
is aJsonObject
with an"app_versions"
field (which is in my experience always empty). - ??? [file transfer protocol goes here]
- Profit
The overall magic-wormhole protocol for the receiver consists of:
- Out of band, obtain the
nameplate
and the ephemeral password from the sender. - Open a websocket to the rendezvous server.
- Establish a channel using the rendezvous protocol on top of the websocket.
- Pick a short hex string at random to be
side
. - Establish a channel using the unencrypted protocol on top of the channel using the rendezvous protocol using
appId
,side
, andnameplate
. - Let
password
be the concatenation of the nameplate string,-
, and the ephemeral password. - Establish a channel using the encrypted protocol on top of the channel using the unencrypted protocol using
appId
,side
, andpassword
. - Send
("version", Utf8Encode(JsonStringify({"app_version":{}})))
over the channel using the encrypted protocol. - Receive a message
theirEncodedVersion
over the channel using the encrypted protocol. - Assert:
JsonParse(Utf8Decode(theirEncodedVersion))
is aJsonObject
with an"app_versions"
field (which is in my experience always empty). - ??? [file transfer protocol goes here]
- Profit
This is the base protocol.
Messages consist of lists of bytes. How to establish the connection and send and receive messages on it is beyond the scope of this document.
Messages consist of (type: string, object: JsonObject)
pairs, where object
has neither id
nor type
keys. (Actually it's more restricted than that, but I'm not going to document it precisely here.)
Given
- a websocket
you can establish a channel using the rendezvous protocol as follows:
- Receive a message from the websocket of the form
Utf8Encode(JsonStringify(object))
, whereobject
is aJsonObject
with atype
key holding the string"welcome"
and a"welcome"
key holding anotherJsonObject
. This second object can have the field"error"
, in which case the connection will terminate.
Once established, you can send a message (type, object)
on this channel as follows:
- Let
id
be a random short-ish hex string. - Let
augmented
by a JsonObject with all of the keys and corresponding values inobject
, as well as new fields named"type"
and"id"
holdingtype
andid
respectively. - Let
encoded
beUtf8Encode(JsonStringify(encoded))
. - Send
encoded
on the websocket.
Once established, you can receive a message (type, object)
on this channel as follows:
- Let
bytes
be a message received from the websocket. - Let
decoded
beJsonParse(Utf8Decode(bytes))
. - Assert:
decoded
has a"type"
field containing a string. - Let
type
be the value of the"type"
field ofdecoded
, and letother
be theJsonObject
holding the remaining keys and corresponding values ofdecoded
. - If
type
is"ack"
, ignore this and do not receive on this channel. - Receive
(type, other)
on this channel.
Sent messages consist of (phase: string, message: list of bytes)
pairs.
Recieved messages consist of (phase: string, side: string, message: list of bytes)
tuples.
Given
- a channel using the rendezvous protocol
appId
, a string identifying the appside
, a short string whose characters are drawn from0-9
anda-f
you as the sender can establish a channel using the unencrypted protocol as follows:
- Send
("bind", { "appid": appId, "side": side })
on the rendezvous channel. - Send
("allocate", {})
on the rendezvous channel. - Receive
("allocated", { "nameplate": nameplate })
on the rendezvous channel, where"nameplate"
is a string. - Send
("claim", { "nameplate": nameplate })
. - Receive
("claimed", { "mailbox": mailbox })
on the rendezvous channel, where"mailbox"
is a string. - Send
("open", { "mailbox": mailbox })
. - Associate
nameplate
with this channel.
Given
- a channel using the rendezvous protocol
appId
, a string identifying the appside
, a short string whose characters are drawn from0-9
anda-f
nameplate
, a short string obtained out of band from the sender
you as the receiver can establish a channel using the unencrypted protocol as follows:
- Send
("bind", { "appid": appId, "side": side })
on the rendezvous channel. - Send
("claim", { "nameplate": nameplate })
. - Receive
("claimed", { "mailbox": mailbox })
on the rendezvous channel, where"mailbox"
is a string. - Send
("open", { "mailbox": mailbox })
. - Associate
nameplate
with this channel.
Once established, you can send a message (phase, message)
on this channel as follows:
- Let
hex
beHexEncode(message)
. - Send
("add", { "phase": phase, "body": body })
on the rendezvous channel.
Once established, you can receive a message (phase, side, message)
on this channel as follows:
- Let
(type, object)
be a message received from the rendezvous channel. - Assert:
object
has"phase"
,"side"
, and"body"
fields each containing a string. - Let
phase
be the value of the"phase"
field ofobject
. - Let
side
be the value of the"side"
field ofobject
. - Let
body
be the value of the"body"
field ofobject
. - Recieve
(phase, side, HexDecode(body))
on this channel.
This is what the magic-wormhole documentation refers to as a "Wormhole".
Messages consist of (phase: string, message: list of bytes)
pairs.
Given
- a channel using the unencrypted protocol
appId
, a string identifying the appside
, a short string whose characters are drawn from0-9
anda-f
password
, a password shared between both parties
you can establish a channel using the encrypted protocol as follows:
- Associate
side
with this channel. - Let
state
(an opaque value) andoutbound
(a list of bytes) be the result of initializing the symmetric SPAKE2 protocol (e.g.) usingUtf8Encode(appId)
as the identity andUtf8Encode(password)
as the password. - Let
encoded
beUtf8Encode(JsonStringify({ "pake_v1": HexEncode(outbound) }))
. - Send
("pake", encoded)
on the unencrypted channel. - Receive
("pake", ignored, theirEncoded)
on the unencrypted channel. - Let
theirDecoded
beJsonParse(Utf8Decode(theirEncoded))
. - Assert:
theirDecoded
has a"pake_v1"
field containing a string. - Let
theirPakeHex
be the value of the"pake_v1"
field oftheirDecoded
. - Let
theirPake
beHexDecode(theirPake)
. - Let
key
(a list of bytes) be the result of finalizing the symmetric SPAKE2 protocol usingstate
andtheirPake
(e.g.). - Associate
key
with this channel.
Once established, you can send a message (phase, message)
on this channel as follows:
- Let
nonce
beGetRandomBytes(24)
. - Let
phaseKey
beDerivePhaseKey(key, side, phase)
. - Let
ciphertext
beMakeSecretBox(message, nonce, phaseKey)
. - Let
full
be the concatenation ofnonce
andciphertext
. - Send
(phase, full)
on the unencrypted channel.
Once established, you can receive a message (phase, message)
on this channel as follows:
- Recieve
(phase, theirSide, message)
from the unencrypted channel. - Let
phaseKey
beDerivePhaseKey(key, theirSide, phase)
. - Let
nonce
be the first 24 bytes ofmessaage
. - Let
ciphertext
be the remaining bytes ofmessage
. - Let
plaintext
beOpenSecretBox(ciphertext, nonce, phaseKey)
. - Receive
(phase, plaintext)
on this channel.
These are types and methods referenced above.
An integer between 0 and 255 inclusive.
A list of unicode code points.
An object with keys and values of the kind supported by JSON. Can be written like { "foo": bar }
.
Return the object given by parsing the input according the JSON specification using the object
nonterminal as the goal. (I.e., only {}
-style values are supported for the purposes of this document.)
Return a string representing the input according to the JSON specificatino.
Return the string given by interpreting the list of bytes as a utf8-encoded sequence of code points according to the unicode spec.
Return a list of bytes given by encoding the input according to the unicode spec.
Return the list of bytes given by hex decoding the input.
Return the string given by hex encoding the input.
From a cryptographically secure source, obtain a list of random bytes of length given by the input.
Return the sha256 digest of the input.
Return the length-32 expansion of key
using HKDF with purpose
as the info
and with no salt.
This algorithm behaves as follows:
- Let
base
beUtf8Encode("wormhole:phase:")
. - Let
sideSha
beSha256(Utf8Encode(side))
. - Let
phaseSha
beSha256(Utf8Encode(phase))
. - Let
purpose
be the list of bytes given by concatenatingbase
,sideSha
, andphaseSha
. - Return
HKDF(key, purpose)
.
MakeSecretBox(plaintext: list of bytes, nonce: list of bytes, phaseKey: list of bytes): list of bytes
Return the result of calling NaCL's secretbox
method using plaintext
as the message, nonce
as the nonce, and phaseKey
as the key.
OpenSecretBox(ciphertext: list of bytes, nonce: list of bytes, phaseKey: list of bytes): list of bytes
Return the result of calling NaCL's secretbox_open
method using ciphertext
as the ciphertext, nonce
as the nonce, and phaseKey
as the key.