Skip to content

Instantly share code, notes, and snippets.

@TheBlueMatt
Last active December 17, 2015 16:46
Show Gist options
  • Save TheBlueMatt/d2fcfb78d29faca117f5 to your computer and use it in GitHub Desktop.
Save TheBlueMatt/d2fcfb78d29faca117f5 to your computer and use it in GitHub Desktop.
TextSecure - Provisioning a new Secondary Device

Goals

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.

Overview

The process, in general, works as follows:

  1. The user opens a registration screen on the secondary device which they wish to register, causing it to request a device code from the server.
  2. The new secondary device displays a QR code for the master device to scan which contains the device code from the server, and a curve25519 public key.
  3. The user selects "provision a new device" on their master device and scans the QR code. The master device requests a new registration code from the server.
  4. 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).
  5. The secondary device registers itself with the server using the provided registration code and identity key.
  6. 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.
  7. The secondary device opens a new channel for DeviceControl messages with the master device and all other existing devices.
  8. 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).

New and Existing APIs

  1. GET /v1/devices/setup_code is added for this purpose. This returns a UUID which can be used as the user in a {basic_auth} header (with a password of "temp"). This auth token may be used to register on a websocket and receive messages, but it may not be used for any other purpose. The secondary device should open a listening websocket before proceeding to step 2.

  2. The QR code should contain a link to textsecure-device-init:{setup_code UUID}/{ephemeral curve25519 public key}.

  3. GET /v1/devices/provisioning_code already exists for this purpose (and may be extended to use codes longer than 6 digits).

  4. 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, below) and sends it, prepended with the constant version byte 1, the AES IV, and a full 32-byte HMAC256(version byte 1 + IV + encrypted data), as the identityKeyMessage field in a DeviceInit message (below).

    message DeviceInit {
      required bytes masterEphemeralKey = 1;
      required bytes identityKeyMessage = 2;
    }
    message IdentityKey {
      required bytes   identityKey       = 1;
      required string  number            = 2;
      required boolean masterSupportsSms = 3; // XXX: boolean is a protobuf type?
      required uint32  provisioningCode  = 3;
    }
  5. PUT /v1/devices/{provisioning_code}, and PUT /v1/keys already exist for this purpose.

  6. The secondary device should hold the websocket created in step 1 until after this step completes.

    1. 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).
    2. 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.
    3. 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).
  7. 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:

    1. OUTSIDE_TRANSPORT_RECEIVED_MESSAGE may be sent from the master device and must be sent to all other devices. It indicates to the secondary devices that a message was received over the SMS/MMS channel. If that message was encrypted, it must be decrypted before being relayed to secondary devices (using the original AttachmentPointer iff that attachment will be available to secondary devices using the same id which is present in the message, otherwise new AttachmentPointers must be created to share attachments with the secondary devices).
    2. 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.
    3. SEND_MESSAGE may be sent from any secondary device to the master device. It requests that the master device forward the given message over the SMS/MMS channel (possibly unencrypted over the wire). After receipt of the message, the master should respond with either a SENT_MESSAGE with the same contents (as it should forward the same SENT_MESSAGE to ALL secondary devices, including the originator) or a SEND_MESSAGE_FAILED.
    4. SEND_MESSAGE_FAILED may be sent from the master device to a single secondary device in response to a SEND_MESSAGE message. Its messageId field should be set to the same value as was set in the SEND_MESSAGE it received and indicates that the message failed to send. The reason field should be set to a short (less than 80 characters), human readable reason why the message could not be delivered, such as "Outgoing SMS not allowed".
message DeviceControl {
  enum Type {
    UNKNOWN                            = 0;
    NEW_DEVICE_REGISTERED              = 1; // Requries only newDeviceId
    SENT_MESSAGE                       = 2; // Requires only message
    SEND_MESSAGE                       = 3; // Requires both message and messageId
    SEND_MESSAGE_FAILED                = 4; // Requires only messageId
    OUTSIDE_TRANSPORT_RECEIVED_MESSAGE = 5; // Requires only message
  }
  message MessageSentReceived {
    enum RelayType {
      PUSH    = 1; // Indicates the message was already sent
      SMS_MMS = 2; // Indicates the message was sent/received by the master,
                   // or that the master device needs to send the message
    }
    required string             otherNumber = 1; // The source/destination account (ie phone #), not device
    required RelayType          relay       = 2; // The relay used to send (always SMS_MMS for OUTSIDE_TRANSPORT_RECEIVED_MESSAGE/SEND_MESSAGE)
    required uint64             timestamp   = 3;
    optional uint32             messageId   = 4; // Only required for SEND_MESSAGE
    required PushMessageContent message     = 5;
  }
  message SendMessageFailed {
    required uint32 messageId = 1;
    required string reason    = 2;
  }
  required Type                type        = 1;
  optional uint32              newDeviceId = 2;
  optional MessageSentReceived message     = 3;
  optional SendMessageFailed   sendFailed  = 4;
}

Notes

Reason why I still included SEND_MESSAGE/SMS stuff in here: Though it is tricky to enable across the board, I think its inclusion represents a feature that merits some complaints over its implementation. I know we have a very vocal community that will, no doubt, have a fun time commenting on the UI surrounding this feature, but I think we can minimize the user complaints and questions here. It seems wrong to me to limit our featureset because we're afraid of getting complaints from other developers (and, yes, I'll gladly help comment on those pulls/emails/etc if y'all forward them this way). I think the UI should (obviously) clearly state that SMS relay will ONLY work when the master device is on (my thought would be a checkbox in the secondary device options that says "Enable sending SMS/MMS messages to non-TextSecure users by relaying them through your phone. Note that this will only work when your phone is on, connected to the network, and TextSecure is set on your phone to allow outgoing SMS/MMS messages." Note that I'm not sure if we need a UI flag to allow relay in the Android side (I vote no) as it simplifies the whole thing. Implementation-wise, I dont think this feature is hard to add, and if it is a source of a lot of user complaints, hiding the checkbox so that users have to know where to go to enable the SMS relay may be a good option (how many users check out the options page for their browser extensions now, anyway?).

@moxie0
Copy link

moxie0 commented May 21, 2014

The newly registered device uploads its prekeys and the identity key and sends the master a IDENTITY_KEY_RECEIVED (see below) DeviceControl message, completing the session init.

Why do we need an ACK here?

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).

I would prefer to immediately tear this one-off session down.

PUT /v1/messages/{destination} is changed to allow a message to be sent to a specific device if the sender is on the same account and {destination} is in the form {number}.{Destination device ID}. New messages can be sent between devices with IncomingPushMessageSignal.Type (and the equivalent message type in /v1/messages/) getting two new values:

I'd ideally like to maintain some consistency here. One possibility would be to have PUT /v1/messages/{destination} remain the same, with the one tweak that the source device can be omitted from the list of destination devices if source = destination.

IDENTITY_KEY_EXCHANGE = 5 indicating a standard PreKeyWhisperMessage with preKeyId set to 0 and message containing a WhisperMessage with the ciphertext set to an encrypted copy of the master device's identity key.

I'd prefer not to put this in the IncomingPushMessage "type." That field is only for values that can not be opaque, for instance values that a receiving client needs in order to determine how to decrypt the message. These seem like values that could be within an encrypted message.

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).

My sense is that we should just leave SMS out of this. If I receive an incoming SMS message on my desktop, I can't reply to it anyway. Also, it'd be way simpler if the only thing we ever had to deliver to provisioned devices was outgoing messages.

@TheBlueMatt
Copy link
Author

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:

  1. Selfishly: if I'm texting someone who doesnt have textsecure, I want to be able to do it from the same app/place and keep history synced. This means if I receive the message on my phone using SMS, I'd like it to appear on my computer where I can respond from and keep history.
  2. Its a feature that we could reasonably advertise for people who don't care about encrypted sms and have no friends using TextSecure. If our goal is to get people using encrypted messaging where possible instead of SMS, having a feature like "Send SMS from your computer!" is a nice property. Of course it gets tricky since this wont work if you're on iOS (need to have a field for that in the protobuf), but I still think its worth it.

@mcginty
Copy link

mcginty commented May 21, 2014

Turning the master device into an SMS transceiver in this protocol makes the master device a service with a certain amount of implied availability for this feature to even be considered a real feature. We would not only need a flag for master devices that don't support SMS, but a way to update those feature indicators for ones that dynamically do and don't, as well as which contexts they have opted are OK to send SMS/MMS (Android's current setup). Since the devices are not ever considered available at any given time, and without delivery receipts, this will probably increase headache for both us and users by a decent amount.

in short: i also vote not including SMS/MMS in this spec

@mcginty
Copy link

mcginty commented May 21, 2014

Also I'd like to clarify what we consider master and secondary devices. Are we going to consider any device with a phone number master (ignoring the case where you can receive a provisioning code on one device and type it in another), and any device without one a secondary? We can discuss this somewhere else though since it's not really related to the protocol and more how we design the applications.

@TheBlueMatt
Copy link
Author

Master for the purpose of this is the first device to register (ie the device with the phone number).

@moxie0
Copy link

moxie0 commented May 21, 2014

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 hear that, but by that logic don't we then need an ACK for the ACK? =) I'd like to see if we can design an appropriate UI without one first. I think @mcginty can probably make it happen.

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.

Because it's not a real session. There's no registration id, for instance. As we move more things into the session, I don't want to have to also transmit them via QR code.

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?

I'd like to eliminate the need for devices to communicate without sending a message to all other devices. It does seem as if the master will need to deliver a single unicast message to the device being provisioned, so we can special case that for devices which are "in-progress."

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.

DeviceControl looks way too complicated to me. I think we can get away with adding a single flag to MessageContent which indicates that the body is an identity key. I think we can do away with all other control messages, once a device is provisioned (a single message) there shouldn't be any need to unicast to that device again.

We can leave SMS out if you like, but I put it in for a few reasons:

My gut instinct is no and hell no. SMS/MMS is a huge fucking pain, and we would be much better off today if we didn't support it anywhere. I'd like to start migrating away from encrypted SMS support, and maybe eventually (baruch hashem) remove SMS support from the Android client all together.

I think what you're describing is definitely a neat feature, but my sense is that we would be absolutely eaten alive by the complexity. This is already an entire product (Mighty Text), which I'm sure is a lot of work for them.

@midi
Copy link

midi commented Jun 2, 2014

i'll let you guys do what you do, building a superb communication channel.

i for one would really appreciate sms support, having synced sms on devices and the ability to send from internet only devices by using the phone, like the mentioned mightytext would be a superb feature.
even just keeping the current sms support on android, really helps me to gently introduce people to textsecure, through a trojan horse like mode, except in a good way.

being able to show them a new sleek client for sms, that additionally also supports "super secure" conversations and also allows them to get in contact with me is much easier than having to introduce yet another messenger, even though this my exclusive messenger.
i can see how this will be even harder for people who aren't as exclusive as i am, with their messenger accounts. of course for iOS this is not valid.

excited for the upcoming features!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment