To provide a method to securely copy a user's identity key from a master device to a newly-provisioned secondary device. Note that we do not cover copying an identity key from an existing secondary device to another secondary device.
The process, in general, works as follows:
- The user opens a registration screen on the secondary device which they wish to register, causing it to request a temporary messaging channel from the server.
- The new secondary device displays a QR code for the master device to scan which contains the messaging channel UID from the server, and a curve25519 public key.
- The user selects "provision a new device" on their master device and scans the QR code. The master device requests a new device registration code from the server.
- The master device encrypts its identity key using a shared key derived from the scanned key and one of its own chosing and sends the message to the secondary device (using the destination provided in the scanned QR code).
- The secondary device registers itself with the server using the provided registration code and identity key.
- Becasue an attacker who is standing behind the secondary device during registration could register the device on their own number, the secondary device must confirm the phone number provided by the master device.
- The secondary device opens a new channel for DeviceControl messages with the master device and all other existing devices.
- The devices continue to use this session to exchange control messages, including sharing sent messages and providing the secondary device(s) with messages received over the SMS channel (either encrypted or unencrypted).
-
GET /v1/temp_websocket is added for this purpose. On connect as a websocket client, the server immidiately posts a message in the form '{uuid: "{socket_uuid}"}' and keeps the socket open. This UUID/websocket can be used to receive a single message (in the form '{message: "{message}"}', without an id field as the server does not wait for an ACK from the client), at which time it is automatically closed, and may not be used for any other purpose. The secondary device should open a listening websocket before proceeding to step 2.
-
The QR code should contain a link to textsecure-device-init:/?channel_uuid={setup_code UUID}&channel_server={server the secondary device is registered on}&publicKey={ephemeral curve25519 public key}.
-
GET /v1/devices/provisioning_code already exists for this purpose (and may be extended to use codes longer than 6 digits).
-
The master device creates a new ephemeral curve25519 key and calculates a shared secret (using ECDH) between it and the scanned key. It then calculates an AES and MAC key by using the standard TextSecure HKDF function with the shared secret as the input, the master device's public key as the salt, and the info string set to 'WhisperDeviceInit'. It uses these keys to encrypt an IdentityKey message (using AES-CBC, in the same way as attachments) and sends it, prepended with the constant version byte (3 << 4) | 3, the AES IV, and appending a full 32-byte HMAC256(version byte + IV + encrypted data), as the identityKeyMessage field in a DeviceInit message (below). The message is sent using a standard send message, with the destination set to the UUID provided in the channel_uuid field and the relay set to the channel_server in step 2.
message DeviceInit { required bytes masterEphemeralPubKey = 1; required bytes identityKeyMessage = 2; // contains an IdentityKey }
message IdentityKey { required bytes identityKey = 1; required string phoneNumber = 2; required string server = 3; required boolean masterSupportsSms = 4; // XXX: boolean is a protobuf type? required uint32 provisioningCode = 5; }
-
PUT /v1/devices/{provisioning_code}, and PUT /v1/keys already exist for this purpose. Note that these calls should be made to the server specified in the IdentityKey.server field, after the client has verified that the given server is one of a whitelist of acceptable/semi-trusted servers.
-
The secondary device should ask the user to verify the phone number and simply restart the process if it does not match, similarly if the master device fails to send its message, it should alert the user that they need to restart the process, and should NOT continue blindly, despite secondary device claims to the contrary.
-
- GET /v1/keys/{number}/* should return the keys required (the implementor may wish to make this call before the PUT /v1/keys in step 5 to avoid retreiving their own key).
- PUT /v1/messages/{destination} is changed to allow a message to be sent to a specific device (ie it will never return 409) if the sender is on the same account.
- New messages can be sent between devices with IncomingPushMessageSignal.Type (and the equivalent message type in /v1/messages/) set to PREKEY_BUNDLE_DEVICE_CONTROL = 5 indicating a standard PreKeyWhisperMessage containing a WhisperMessage with the ciphertext set to an encrypted copy of a DeviceControl message of type NEW_DEVICE_REGISTERED (below).
-
IncomingPushMessageSignal.Type (and the equivalent message type in /v1/messages/) may also be set to DEVICE_CONTROL = 6 indicating a WhisperMessage with a ciphertext set to an encrypted copy of a DeviceControl message (below). This allows the devices to send the following messages:
- SENT_MESSAGE may be sent from any device and must be sent to all other devices. It indicates the successful sending of a message over the specified channel.
- NEW_DEVICE_REGISTERED is sent in step 5.3.
message DeviceControl {
enum Type {
UNKNOWN = 0;
NEW_DEVICE_REGISTERED = 1; // Requries only newDeviceId
SENT_MESSAGE = 2; // Requires only message
}
message MessageSent {
required string otherNumber = 1; // The destination account (ie phone #), not device
required uint64 timestamp = 2;
required bytes message = 3; // PushMessageContent
}
required Type type = 1;
optional uint32 newDeviceId = 2;
optional MessageSent message = 3;
}
An alternative to section 8 and the DeviceControl proto can be found at https://gist.github.com/TheBlueMatt/d2fcfb78d29faca117f5/fa0b9d5a6bf026f12a6a35e63b62a0b3a446467d
The relay stuff is gross, but its easy to implemnt here (keep a static list of only one server option and fail if its different), and its nice for upgrade-ability.
Note that the DEVICE_CONTROL message contents do not leak information as the server can already assume messages between devices on the same account are DEVICE_CONTROL messages.
I prefer the ACK after setup to deal with the message-lost case. ie think about the UI flow that looks similar to the original registration UI, I'd like to only close that window after we are actually sure of the success, which means sending an ACK. That said, of course, the server is pretty reliable anyway, so if you dont want it it can pretty easily be dropped.
I see no reason to tear down the one-off session. After the ACK we've already stepped the ratchet so there is almost no difference between keeping it there and tearing it down, aside from needing yet more random bytes to create new keys.
We can do that, but I like the n-to-n communication where each device communicates with each other device using its own session. In this case, you need to be able to send to only the one device you want. If you want more consistency, we can just change from messages to devices or so?
My impression of the IncomingPushMessage type was "if the server can tell based on your behavior what kind of message it is, its neater to split up the message types into different protobufs and then tell the server what type it is so the client knows how to decrypt". Note that because I prefer to use a different protobuf for the DeviceControl stuff we would need a different IncomingPushMessage type here. Note, however, that I kinda broke the above rule with the new move-everything-into-DeviceControl changes, but I prefer that. If you do really want to stick with PREKEY_BUNDLEs and CIPHERTEXT messages, we'd need to add a type field to WhisperMessage to indicate the presence of a DeviceControl.
We can leave SMS out if you like, but I put it in for a few reasons: