nuntius is an iOS framework that helps iOS developers integrate end-to-end encryption (e2ee) into their apps with simple APIs. It provides an objc implementation of the Extended Triple Diffie-Hellman (X3DH) and Double Ratchet protocols using libsodium for most of the crypto operations. nuntius provides Authenticated Encryption with Associated Data (AEAD) via AES-CBC-HMAC-256, it uses Apple's CommonCrypto framework for these operations, but in the future I'll move to libsodium-only crypto and use ChaCha20-Poly1305 instead.
As described here, X3DH is a key agreement protocol that establishes a shared session key between two parties that mutually authenticate each other based on public keys. nuntius
uses:
- Curve25519 for elliptic curve public key cryptography (ECDH)
- Ed25519 for public-key signatures
- SHA256 hashing algorithm
- BLAKE2b as KDF for key derivation
As described here, the Double Ratchet protocol is used after a shared session key is established between two parties (for example with X3DH) to send and receive encrypted messages. It provides forward secrecy (FS) by deriving new encryption keys after every Double Ratchet message, meaning that if an encryption key is compromised, it cannot be used to decrypt old messages. It provides a symmetric encryption key ratachet and a Diffie-Hellman public key encryption ratachet, this is why is called Double Ratchet. nuntius
uses:
- Curve25519 for elliptic curve public key cryptography (ECDH)
- BLAKE2b as KDF for key derivation
- AES-256 in CBC mode with PKCS#7 padding for symmectric encryption
- HMAC-256 for Authenticated Encryption
pod "nuntius"
In Objc import the nuntius header
#import <nuntius/nuntius.h>
In Swift import the nuntius module
import nuntius
Objc
IREncryptionService *IREncryptionService = [IREncryptionService new];
IRCurve25519KeyPair *keyPair = [IREncryptionService generateKeyPair];
NSLog(@"Public Key: %@", keyPair.publicKey);
NSLog(@"Private Key: %@", keyPair.privateKey);
Swift
let IREncryptionService = IREncryptionService()
let keyPair = IREncryptionService.generateKeyPair()!
print("Public Key: \(keyPair.publicKey)")
print("Private Key: \(keyPair.privateKey)")
Objc
IREncryptionService *IREncryptionService = [IREncryptionService new];
IRCurve25519KeyPair *signingKeyPair = [IREncryptionService generateKeyPair];
IRCurve25519KeyPair *signedKeyPair = [IREncryptionService generateKeyPair];
NSData *signature = [IREncryptionService signData:signedKeyPair.publicKey withKeyPair:signingKeyPair];
[signedKeyPair addKeyPairSignature:signature];
NSLog(@"Signature: %@", signedKeyPair.signature);
Swift
let IREncryptionService = IREncryptionService()
let signingKeyPair = IREncryptionService.generateKeyPair()!
let signedKeyPair = IREncryptionService.generateKeyPair()!
guard let signature = IREncryptionService.sign(signedKeyPair.publicKey, with: signingKeyPair) else {
return
}
signedKeyPair.addSignature(signature)
print("Signature: \(signedKeyPair.signature!)")
Objc
IREncryptionService *IREncryptionService = [IREncryptionService new];
IRCurve25519KeyPair *signingKeyPair = //get signing key pair
IRCurve25519KeyPair *signedKeyPair = //get signed key pair
BOOL valid = [IREncryptionService verifySignature:signedKeyPair.signature ofRawData:signedKeyPair.publicKey withKeyPair:signingKeyPair];
if (valid) {
NSLog(@"Valid signature");
} else {
NSLog(@"Invalid signature");
}
Swift
let IREncryptionService = IREncryptionService()
let signingKeyPair = //get signing key pair
let signedKeyPair = //get signed key pair
let valid = IREncryptionService.verifySignature(signedKeyPair.signature!, ofRawData: signedKeyPair.publicKey, with: signingKeyPair)
if valid {
print("Valid signature")
} else {
print("Invalid signature")
}
Objc
IREncryptionService *IREncryptionService = [IREncryptionService new];
//Alice is the sender
IRCurve25519KeyPair *aliceKeyPair = [IREncryptionService generateKeyPair];
//Bob is the receiver
IRCurve25519KeyPair *bobKeyPair = [IREncryptionService generateKeyPair];
NSData *sharedSecretAlice = [IREncryptionService senderSharedKeyWithRecieverPublicKey:bobKeyPair.publicKey andSenderKeyPair:aliceKeyPair];
NSData *sharedSecretBob = [IREncryptionService receiverSharedKeyWithSenderPublicKey:aliceKeyPair.publicKey andReceiverKeyPair:bobKeyPair];
if ([sharedSecretAlice isEqualToData:sharedSecretBob]) {
NSLog(@"Success!");
} else {
NSLog(@"Error!");
}
Swift
let IREncryptionService = IREncryptionService()
//Alice is the sender
let aliceKeyPair = IREncryptionService.generateKeyPair()!
//Bob is the receiver
let bobKeyPair = IREncryptionService.generateKeyPair()!
let sharedSecretAlice = IREncryptionService.senderSharedKey(withRecieverPublicKey: bobKeyPair.publicKey, andSenderKeyPair: aliceKeyPair)
let sharedSecretBob = IREncryptionService.receiverSharedKey(withSenderPublicKey: aliceKeyPair.publicKey, andReceiverKeyPair: bobKeyPair)
guard sharedSecretAlice != nil, sharedSecretBob != nil, sharedSecretAlice == sharedSecretBob else {
return
}
print("Success!")
Objc
IREncryptionService *IREncryptionService = [IREncryptionService new];
NSData *sharedSecret = //some shared secret, for example ECDH(alice.privateKey,bob.publicKey)
/* MIN outputLength: 16, MAX outputLength: 64, Salt: key id */
NSData *key1 = [IREncryptionService sharedKeyKDFWithSecret:sharedSecret andSalt:1 outputLength:32];
NSData *key2 = [IREncryptionService sharedKeyKDFWithSecret:sharedSecret andSalt:2 outputLength:64];
NSLog(@"Derived Key1: %@", key1);
NSLog(@"Derived Key2: %@", key2);
Swift
let IREncryptionService = IREncryptionService()
let sharedSecret = //some shared secret, for example ECDH(alice.privateKey,bob.publicKey)
/* MIN outputLength: 16, MAX outputLength: 64, Salt: key id */
let key1 = IREncryptionService.sharedKeyKDF(withSecret: sharedSecret, andSalt: 1, outputLength: 32)
let key2 = IREncryptionService.sharedKeyKDF(withSecret: sharedSecret, andSalt: 2, outputLength: 64)
print("Key 1 \(key1!)")
print("Key 2 \(key2!)")
There is also:
- (NSData * _Nullable)rootKeyKDFWithSecret:(NSData * _Nonnull)secret
andSalt:(uint64_t)salt
outputLength:(NSUInteger)outputLength;
- (NSData * _Nullable)chainKeyKDFWithSecret:(NSData * _Nonnull)secret
andSalt:(uint64_t)salt
outputLength:(NSUInteger)outputLength;
- (NSData * _Nullable)messageKeyKDFWithSecret:(NSData * _Nonnull)secret
andSalt:(uint64_t)salt
outputLength:(NSUInteger)outputLength;
All of these methods can be used for deriving keys, they are convenience methods for the Double Ratchet protocol. Internally they all use the same method but with different ctx
(one of libsodium's crypto_kdf_derive_from_key
parameter)
Objc
IREncryptionService *IREncryptionService = [IREncryptionService new];
NSData *data = [@"my-secret-data" dataUsingEncoding:NSUTF8StringEncoding];
NSData *aesKey = //Random AES key, for example key = KDF(sharedSecret, 1, 32)
NSData *hmacKey = //Random HMAC key, for example key = KDF(sharedSecret, 2, 32)
NSData *iv = //Random IV, for example key = KDF(sharedSecret, 3, 16)
NSMutableData *ratchetData = [NSMutableData new];
//Add Sender Ratchet Public Key
[ratchetData appendData:senderKeyPair.publicKey];
//Add Number of Sent Messages
const char numberOfSentMessages[1] = {0x0};
[ratchetData appendBytes:numberOfSentMessages length:sizeof(numberOfSentMessages)];
//Add Number of Sent Messages in Previous Chain
const char numberOfSentMessagesInPreviousChain[1] = {0x0};
[ratchetData appendBytes:numberOfSentMessagesInPreviousChain length:sizeof(numberOfSentMessagesInPreviousChain)];
NSError *error = nil;
NSData *ciphertext = [IREncryptionService aeEncryptData:data: symmetricKey:aesKey hmacKey:hmacKey iv:iv ratchetHeader:ratchetData error:&error];
if (error == nil) {
NSLog(@"Encrypted data: %@", ciphertext);
//Decrypting
NSError *decryptionError = nil;
NSData *plaintext = [IREncryptionService aeDecryptData:ciphertext symmetricKey:aesKey hmacKey:hmacKey iv:iv error:&decryptionError];
if (decryptionError == nil) {
NSLog(@"Plaintext: %@",plaintext);
} else {
NSLog(@"Error: %@", decryptionError);
}
} else {
NSLog(@"Error: %@", error);
}
Swift
let IREncryptionService = IREncryptionService()
let data = "my-secret-data".data(using: .utf8)!
let aesKey = //Random AES key, for example key = KDF(sharedSecret, 1, 32)
let hmacKey = //Random HMAC key, for example key = KDF(sharedSecret, 2, 32)
let iv = //Random IV, for example key = KDF(sharedSecret, 3, 16)
var ratchetData = Data()
//Add Sender Ratchet Public Key
ratchetData.append(aliceKeyPair.publicKey)
//Add Number of Sent Messages
let numberOfSentMessages: Int32 = 0x0
var beNumberOfSentMessages = numberOfSentMessages.bigEndian
ratchetData.append(UnsafeBufferPointer(start: &beNumberOfSentMessages, count: 1))
//Add Number of Sent Messages in Previous Chain
let numberOfSentMessagesInPreviousChain: Int32 = 0x0
var beNumberOfSentMessagesInPreviousChain = numberOfSentMessagesInPreviousChain.bigEndian
ratchetData.append(UnsafeBufferPointer(start: &beNumberOfSentMessagesInPreviousChain, count: 1))
do {
let ciphertext = try IREncryptionService.aeEncryptData(data, symmetricKey: aesKey, hmacKey: hmacKey, iv: iv, ratchetHeader: ratchetData)
print("Encrypted data: \(ciphertext)")
//Decrypting
let plaintext = try IREncryptionService.aeDecryptData(ciphertext, symmetricKey: aesKey, hmacKey: hmacKey, iv: iv)
print("Plaintext: \(plaintext)")
} catch {
print("Error \(error)")
}
You can read more about what the Ratchet Header
is and why is it needed here. For encrypting/decrypting data outside the Double Ratchet protocol there is also:
- (NSData * _Nullable)aeEncryptSimpleData:(NSData * _Nonnull)plaintextData
symmetricKey:(NSData * _Nonnull)symmetricKey
hmacKey:(NSData * _Nonnull)hmacKey
iv:(NSData * _Nonnull)iv
error:(NSError * _Nullable * _Nullable)error;
- (NSData * _Nullable)aeDecryptSimpleData:(NSData * _Nonnull)cipherData
symmetricKey:(NSData * _Nonnull)symmetricKey
hmacKey:(NSData * _Nonnull)hmacKey
iv:(NSData * _Nonnull)iv
error:(NSError * _Nullable * _Nullable)error;
Objc
//Alice is the sender and Bob is the receiver
IRDoubleRatchetService *aliceDoubleRatchet = [IRDoubleRatchetService new];
NSData *aliceSharedSecret = //some shared secret, for example ECDH(alice.privateKey,bob.publicKey)
[aliceDoubleRatchet setupRatchetForSendingWithSharedKey:aliceSharedSecret andDHReceiverKey:bobKeyPair];
IRDoubleRatchetService *bobDoubleRatchet = [IRDoubleRatchetService new];
NSData *bobSharedSecret = //some shared secret, for example ECDH(bob.privateKey,alice.publicKey)
[bobDoubleRatchet setupRatchetForReceivingWithSharedKey:bobSharedSecret andDHSenderKey:alice.signedPreKeyPair];
//Sending messages
NSData *message = //some message
NSError *error = nil;
NSData *ciphertext = [aliceDoubleRatchet encryptData:message error:&error];
if (error == nil) {
NSLog(@"Ready to send message: %@", ciphertext);
} else {
NSLog(@"Error %@",error);
}
//Receiving message
NSData *plaintext = [bobDoubleRatchet decryptData:ciphertext error:&error];
if (error == nil) {
NSLog(@"Message received: %@", plaintext);
} else {
NSLog(@"Error %@",error);
}
Swift
//Alice is the sender and Bob is the receiver
let aliceDoubleRatchet = IRDoubleRatchetService()
let aliceSharedSecret = //some shared secret, for example ECDH(alice.privateKey,bob.publicKey)
aliceDoubleRatchet.setupRatchetForSending(withSharedKey: aliceSharedSecret, andDHReceiverKey: bobKeyPair)
let bobDoubleRatchet = IRDoubleRatchetService()
let bobSharedSecret = //some shared secret, for example ECDH(bob.privateKey,alice.publicKey)
bobDoubleRatchet.setupRatchetForReceiving(withSharedKey: bobSharedSecret, andDHSenderKey: alice.signedPreKeyPair)
//Sending messages
let message = //some message
do {
let ciphertext = try aliceDoubleRatchet.encryptData(message)
print("Ready to send message: \(ciphertext)")
//Receiving message
let plaintext = try bobDoubleRatchet.decryptData(ciphertext)
print("Message received \(plaintext)")
} catch {
print("Error \(error)")
}
Objc
NSDictionary<NSString *, NSString *> *state = [someDoubleRatchet doubleRatchetState];
//store state in local encrypted db
IRDoubleRatchetService *doubleRatchet = [IRDoubleRatchetService new];
NSDictionary<NSString *, NSString *> *state = //get double ratchet state from local encrypted DB
[doubleRatchet setupWithRatchetState:state];
Swift
let state = someDoubleRatchet.doubleRatchetState()
//store state in local encrypted db
let doubleRatchet = IRDoubleRatchetService()
let state = //get double ratchet state from local encrypted DB
doubleRatchet.setup(withRatchetState: state)
Objc
IRCurve25519KeyPair *identityKeyPair = //generate IRCurve25519KeyPair
IRCurve25519KeyPair *signedPreKeyPair = //generate IRCurve25519KeyPair
NSArray<IRCurve25519KeyPair *> *ephemeralKeyPairs = //generate "X" IRCurve25519KeyPairs
IRTripleDHService *tripleDH = [[IRTripleDHService alloc] initWithIdentityKeyPair:identityKeyPair signedPreKeyPair:signedPreKeyPair ephemeralKeys:ephemeralKeyPairs];
//When sending a message
IRCurve25519KeyPair *rIdentityKey = //get receiver's identity key from server or db
IRCurve25519KeyPair *rSignedPreKey = //get receiver's signed pre-key from server or db
IRCurve25519KeyPair *rEphemeralKey = //get one of the receiver's ephemeral keys from server
NSData *sharedKey = [tripleDH sharedKeyFromReceiverIdentityKey:rIdentityKey receiverSignedPreKey:rSignedPreKey receiverEphemeralKey:rEphemeralKey];
//Use sharedKey
//When receiving a message
IRCurve25519KeyPair *sIdentityKey = //get sender's identity key from server or db
IRCurve25519KeyPair *sEphemeralKey = //get sender's signed pre-key from server or db
NSString *rEphemeralKeyID = //get ephemeral key id from received message's heeader
NSData *sharedKey = [tripleDH sharedKeyFromSenderIdentityKey:sIdentityKey senderEphemeralKey:sEphemeralKey receiverEphemeralKeyID:rEphemeralKeyID];
//Use sharedKey
Swift
let identityKeyPair = //generate IRCurve25519KeyPair
let signedPreKeyPair = //generate IRCurve25519KeyPair
let ephemeralKeyPairs: Array<IRCurve25519KeyPair> = //generate "X" IRCurve25519KeyPairs
let tripleDH = IRTripleDHService(identityKeyPair: identityKeyPair, signedPreKeyPair: signedPreKeyPair, ephemeralKeys: ephemeralKeyPairs)
//When sending a message
let rIdentityKey = //get receiver's identity key from server or db
let rSignedPreKey = //get receiver's signed pre-key from server or db
let rEphemeralKey = //get one of the receiver's ephemeral keys from server
let sharedKey = tripleDH.sharedKey(fromReceiverIdentityKey: rIdentityKey, receiverSignedPreKey: rSignedPreKey, receiverEphemeralKey: rEphemeralKey)
//Use sharedKey
//When receiving a message
let sIdentityKey = //get sender's identity key from server or db
let sEphemeralKey = //get sender's signed pre-key from server or db
let rEphemeralKeyID: String = //get ephemeral key id from received message's heeader
let sharedKey = tripleDH.sharedKey(fromSenderIdentityKey: sIdentityKey, senderEphemeralKey: sEphemeralKey, receiverEphemeralKeyID: rEphemeralKeyID)
//Use sharedKey
Do you want to contribute? awesome! I'd love to see some PRs opened here.
- Add Examples/Usage
- [] Add Documentation
- [] Create wiki
- Add project to Travis CI
- The Extended Triple Diffie-Hellman and Double Ratchet protocols' implementations where developed from scratch and do not share any source code with existing libraries.
- This library has no relation and is not backed nor supported by the authors of the X3DH and Double Ratchet protocols.