A Swift implementation of the Signal Protocol. The Signal Protocol can be used for secure, end-to-end encrypted messaging in synchronous and asynchronous environments. It has many desirable cryptographic features and can handle missing and out-of-order messages. The Signal protocol is used by the Signal Messenger as well as WhatsApp, Facebook, Skype and others. Additional information can be found here.
This Swift library is intended for educational purposes only, in order to show the way the Signal Protocol works. It somewhat mimics the functionality and structure of the Signal Protocol C implementation.
You can install LibSignalProtocolSwift
through Cocoapods, by adding the following to your Podfile
:
pod 'LibSignalProtocolSwift', '~> 1.3'
After installation the Framework can be accessed by importing it:
import SignalProtocol
The Signal Protocol needs local storage for message keys, identities and other state information.
You can provide this functionality by implementing the protocol KeyStore
, which requires
four delegates for the individual data stores:
IdentityKeyStore
for storing and retrieving identity keysPreKeyStore
for storing and retrieving pre keysSessionStore
for storing and retrieving the sessionsSignedPreKeyStore
for storing and retrieving signed pre keys
There is a feature for group updates, where only one administrator can send, and the others can only receive. If you want this functionality, then implement the GroupKeyStore
protocol, with the additional delegate SenderKeyStore
for storing and retrieving sender keys.
You can have a look at the test implementation for inspiration.
The server that stores the messages for retrieval needs to store the following data for each SignalAddress
:
Public Identity Key
: The public part of the identity key of the deviceSigned Pre Key
: The current signed pre keyPre Keys
: A number of unsigned pre keysMessages
: The messages to deliver to that address, including the sender
The standard process to establish an encrypted session between two devices (two distinct SignalAddress
es) is usually as follows:
- Alice uploads her
Identity
and aSignedPreKey
to the server, as well as a number of unsignedPreKey
s. - Bob retrieves a
PreKeyBundle
from the server, consisting of Alice'sIdentity
, theSignedPreKey
, and one of thePreKey
s (which is then deleted from the server). - Bob creates a session by processing the
PreKeyBundle
and encrypting aPreKeyMessage
which he uploads to the server. - Alice receives Bob's
PreKeyMessage
from the server and decryptes the message. - The encrypted session is established for both Alice and Bob.
Before any secure communication can happen, at least one user needs to upload all necessary ingredients for a PreKeyBundle
to the server.
// Create the identity key ata install time
let identity = try SignalCrypto.generateIdentityKeyPair()
// Store the data in the key store
// Get the public key from the store
let publicKey: Data = try bobStore.getPublicIdentityKey()
// Create pre keys and save them in the store
let preKeys: [Data] = try bobStore.createPreKeys(count: 10)
// Create a signed pre key and save it in the store
let signedPreKey: Data = try bobStore.updateSignedPrekey()
// Upload publicKey, preKeys, and signedPreKey to the server
Let's assume that Alice (who has the SignalAddress
aliceAddress) wants to establish a session with Bob (SignalAddress
bobAddress)
// Download Bob's identity, current signedPreKey and one of the preKeys from the server
// Create PreKeyBundle
let preKeyBundle = try SessionPreKeyBundle(
preKey: preKey,
signedPreKey: signedPreKey,
identityKey: identity)
// Create a new session by processing the PreKeyBundle
let session = SessionCipher(store: aliceStore, remoteAddress: bobAddress)
try session.process(preKeyBundle: preKeyBundle)
// The message to encrypt
let message = "Hello Bob, it's Alice".data(using: .utf8)!
// Here Alice can send messages to Bob
let encryptedMessage = try session.encrypt(message)
// Upload the message to the server
Let's continue the above example and assume Bob receives the message from Alice. Bob can then establish the session:
// Get the message from the server
// Create the session
let session = SessionCipher(store: bobStore, remoteAddress: aliceAddress)
// Process the message
let decryptedMessage = try session.decrypt(preKeyMessage)
Now Alice and Bob can both send and receive messages at will.
// Compose a message
let message = "Hello there".data(using: .utf8)!
// Send message to Bob
let session = SessionCipher(store: aliceStore, remoteAddress: bobAddress)
// Encrypt
let encryptedMessage = try session.encrypt(message)
// Get message from the server
// Receive message from Alice
let session = SessionCipher(store: bobStore, remoteAddress: aliceAddress)
// Decrypt
let decryptedMessage = try session.decrypt(message)
To prevent man-in-the-middle attacks it can be beneficial to compare the identity keys either by manually comparing the fingerprints or through scanning some sort of code (e.g. a QR-Code). The library provides a convenient way for this:
// Create the fingerprint
let aliceFP = try aliceStore.fingerprint(for: bobAddress, localAddress: aliceAddress)
// Display the string...
let display = fingerprint.displayText
// ... or transmit the scannable data to the other client...
let scanData = try fingerprint.scannable.protoData()
// ... or compare to a received fingerprint
fingerprint.matches(scannedFingerprint)
The library is designed to allow different identifiers to distinguish between the different users.
The test implementation uses the SignalAddress
struct for this, which consists of a String
(e.g. a phone number)
and an Int
, the deviceId
. However it is possible to use different structs, classes, or types, as long as they
conform to the Hashable
, Equatable
and CustomStringConvertible
protocols. For example, simple strings can be used:
class MyCustomKeyStore: KeyStore {
typealias Address = String
...
}
Now, SessionCipher can be instantiated, using MyCustomKeyStore
:
let aliceStore = MyCustomKeyStore()
let session = SessionCipher(store: aliceStore, remoteAddress: "Bob")
It is possible for any custom implementation of the SignalCryptoProvider
protocol
to serve as the cryptographic backbone of the protocol. This can be done by
setting the static provider
variable of the SignalCrypto
class:
SignalCrypto.provider = MyCustomCryptoProvider()
The elliptic curve functions are handled by the same C code that is deployed in libsignal-protocol-c and which is packaged in the Curve25519 framework to make the functions available in Swift.
The project is documented heavily because it helps other people understand the code. The documentation is created with jazzy, which creates awesome, apple-like docs.
The docs can be (re-)generated by running the following in the project directory:
jazzy --min-acl private -a 'Christoph Hagen' -u 'https://github.com/christophhagen' -g 'https://github.com/christophhagen/LibSignalProtocolSwift' -e 'Sources/ProtocolBuffers/*' -o 'Documentation'
This code is NOT intended for production use! The code is neither reviewed for errors nor written by an expert. Please do not implement your own cryptographic software, if you don't know EXACTLY what you are doing.