import CryptoSwift
import PromiseKit
import SessionMetadataKit
@objc ( LKSharedSenderKeysImplementation )
public final class SharedSenderKeysImplementation : NSObject , SharedSenderKeysProtocol {
private static let gcmTagSize : UInt = 16
private static let ivSize : UInt = 12
// MARK: D o c u m e n t a t i o n
// A q u i c k o v e r v i e w o f h o w s h a r e d s e n d e r k e y b a s e d c l o s e d g r o u p s w o r k :
//
// • W h e n a u s e r c r e a t e s a g r o u p , t h e y g e n e r a t e a k e y p a i r f o r t h e g r o u p a l o n g w i t h a r a t c h e t f o r
// e v e r y m e m b e r o f t h e g r o u p . T h e y b u n d l e t h i s t o g e t h e r w i t h s o m e o t h e r g r o u p i n f o s u c h a s t h e g r o u p
// n a m e i n a ` C l o s e d G r o u p U p d a t e M e s s a g e ` a n d s e n d t h a t u s i n g e s t a b l i s h e d c h a n n e l s t o e v e r y m e m b e r o f
// t h e g r o u p . N o t e t h a t b e c a u s e a u s e r c a n o n l y p i c k f r o m t h e i r e x i s t i n g c o n t a c t s w h e n s e l e c t i n g
// t h e g r o u p m e m b e r s t h e y s h o u l d n ' t n e e d t o e s t a b l i s h s e s s i o n s b e f o r e b e i n g a b l e t o s e n d t h e
// ` C l o s e d G r o u p U p d a t e M e s s a g e ` . A n o t h e r w a y t o o p t i m i z e t h e p e r f o r m a n c e o f t h e g r o u p c r e a t i o n p r o c e s s
// i s t o b a t c h f e t c h t h e d e v i c e l i n k s o f a l l m e m b e r s i n v o l v e d a h e a d o f t i m e , r a t h e r t h a n l e t t i n g
// t h e s e n d i n g p i p e l i n e d o i t s e p a r a t e l y f o r e v e r y u s e r t h e ` C l o s e d G r o u p U p d a t e M e s s a g e ` i s s e n t t o .
// • A f t e r t h e g r o u p i s c r e a t e d , e v e r y u s e r p o l l s f o r t h e p u b l i c k e y a s s o c i a t e d w i t h t h e g r o u p .
// • U p o n r e c e i v i n g a ` C l o s e d G r o u p U p d a t e M e s s a g e ` o f t y p e ` . n e w ` , a u s e r s e n d s s e s s i o n r e q u e s t s t o a l l
// o t h e r m e m b e r s o f t h e g r o u p t h e y d o n ' t y e t h a v e a s e s s i o n w i t h f o r r e a s o n s o u t l i n e d b e l o w .
// • W h e n a u s e r s e n d s a m e s s a g e t h e y s t e p t h e i r r a t c h e t a n d u s e t h e r e s u l t i n g m e s s a g e k e y t o e n c r y p t
// t h e m e s s a g e .
// • W h e n a n o t h e r u s e r r e c e i v e s t h a t m e s s a g e , t h e y s t e p t h e r a t c h e t a s s o c i a t e d w i t h t h e s e n d e r a n d
// u s e t h e r e s u l t i n g m e s s a g e k e y t o d e c r y p t t h e m e s s a g e .
// • W h e n a u s e r l e a v e s o r i s k i c k e d f r o m a g r o u p , a l l m e m b e r s m u s t g e n e r a t e n e w r a t c h e t s t o e n s u r e t h a t
// r e m o v e d u s e r s c a n ' t d e c r y p t m e s s a g e s g o i n g f o r w a r d . T o t h i s e n d e v e r y u s e r d e l e t e s a l l r a t c h e t s
// a s s o c i a t e d w i t h t h e g r o u p i n q u e s t i o n u p o n r e c e i v i n g a g r o u p u p d a t e m e s s a g e t h a t i n d i c a t e s t h a t
// a u s e r l e f t . T h e y t h e n g e n e r a t e a n e w r a t c h e t f o r t h e m s e l v e s a n d s e n d i t o u t t o a l l m e m b e r s o f
// t h e g r o u p ( a g a i n f e t c h i n g d e v i c e l i n k s a h e a d o f t i m e ) . T h e u s e r s h o u l d a l r e a d y h a v e e s t a b l i s h e d
// s e s s i o n s w i t h a l l o t h e r m e m b e r s a t t h i s p o i n t b e c a u s e o f t h e b e h a v i o r o u t l i n e d a f e w p o i n t s a b o v e .
// • W h e n a u s e r a d d s a n e w m e m b e r t o t h e g r o u p , t h e y g e n e r a t e a r a t c h e t f o r t h a t n e w m e m b e r a n d
// s e n d t h a t b u n d l e d i n a ` C l o s e d G r o u p U p d a t e M e s s a g e ` t o t h e g r o u p . T h e y s e n d a
// ` C l o s e d G r o u p U p d a t e M e s s a g e ` w i t h t h e n e w l y g e n e r a t e d r a t c h e t b u t a l s o t h e e x i s t i n g r a t c h e t s o f
// e v e r y o t h e r m e m b e r o f t h e g r o u p t o t h e u s e r t h a t j o i n e d .
// MARK: R a t c h e t i n g E r r o r
public enum RatchetingError : LocalizedError {
case loadingFailed ( groupPublicKey : String , senderPublicKey : String )
case messageKeyMissing ( targetKeyIndex : UInt , groupPublicKey : String , senderPublicKey : String )
public var errorDescription : String ? {
switch self {
case . loadingFailed ( let groupPublicKey , let senderPublicKey ) : return " Couldn't get ratchet for closed group with public key: \( groupPublicKey ) , sender public key: \( senderPublicKey ) . "
case . messageKeyMissing ( let targetKeyIndex , let groupPublicKey , let senderPublicKey ) : return " Couldn't find message key for old key index: \( targetKeyIndex ) , public key: \( groupPublicKey ) , sender public key: \( senderPublicKey ) . "
}
}
}
// MARK: I n i t i a l i z a t i o n
@objc public static let shared = SharedSenderKeysImplementation ( )
private override init ( ) { }
// MARK: P r i v a t e / I n t e r n a l A P I
internal func generateRatchet ( for groupPublicKey : String , senderPublicKey : String , using transaction : YapDatabaseReadWriteTransaction ) -> ClosedGroupRatchet {
let rootChainKey = Data . getSecureRandomData ( ofSize : 32 ) ! . toHexString ( )
let ratchet = ClosedGroupRatchet ( chainKey : rootChainKey , keyIndex : 0 , messageKeys : [ ] )
Storage . setClosedGroupRatchet ( for : groupPublicKey , senderPublicKey : senderPublicKey , ratchet : ratchet , using : transaction )
return ratchet
}
private func step ( _ ratchet : ClosedGroupRatchet ) throws -> ClosedGroupRatchet {
let nextMessageKey = try HMAC ( key : Data ( hex : ratchet . chainKey ) . bytes , variant : . sha256 ) . authenticate ( [ UInt8 ( 1 ) ] )
let nextChainKey = try HMAC ( key : Data ( hex : ratchet . chainKey ) . bytes , variant : . sha256 ) . authenticate ( [ UInt8 ( 2 ) ] )
let nextKeyIndex = ratchet . keyIndex + 1
return ClosedGroupRatchet ( chainKey : nextChainKey . toHexString ( ) , keyIndex : nextKeyIndex , messageKeys : ratchet . messageKeys + [ nextMessageKey . toHexString ( ) ] )
}
// / - N o t e : S y n c . D o n ' t c a l l f r o m t h e m a i n t h r e a d .
private func stepRatchetOnce ( for groupPublicKey : String , senderPublicKey : String , using transaction : YapDatabaseReadWriteTransaction ) throws -> ClosedGroupRatchet {
#if DEBUG
assert ( ! Thread . isMainThread )
#endif
guard let ratchet = Storage . getClosedGroupRatchet ( for : groupPublicKey , senderPublicKey : senderPublicKey ) else {
let error = RatchetingError . loadingFailed ( groupPublicKey : groupPublicKey , senderPublicKey : senderPublicKey )
print ( " [Loki] \( error . errorDescription ! ) " )
throw error
}
do {
let result = try step ( ratchet )
Storage . setClosedGroupRatchet ( for : groupPublicKey , senderPublicKey : senderPublicKey , ratchet : result , using : transaction )
return result
} catch {
print ( " [Loki] Couldn't step ratchet due to error: \( error ) . " )
throw error
}
}
// / - N o t e : S y n c . D o n ' t c a l l f r o m t h e m a i n t h r e a d .
private func stepRatchet ( for groupPublicKey : String , senderPublicKey : String , until targetKeyIndex : UInt , using transaction : YapDatabaseReadWriteTransaction ) throws -> ClosedGroupRatchet {
#if DEBUG
assert ( ! Thread . isMainThread )
#endif
guard let ratchet = Storage . getClosedGroupRatchet ( for : groupPublicKey , senderPublicKey : senderPublicKey ) else {
let error = RatchetingError . loadingFailed ( groupPublicKey : groupPublicKey , senderPublicKey : senderPublicKey )
print ( " [Loki] \( error . errorDescription ! ) " )
throw error
}
if targetKeyIndex < ratchet . keyIndex {
// T h e r e ' s n o n e e d t o a d v a n c e t h e r a t c h e t i f t h i s i s i n v o k e d f o r a n o l d k e y i n d e x
guard ratchet . messageKeys . count > targetKeyIndex else {
let error = RatchetingError . messageKeyMissing ( targetKeyIndex : targetKeyIndex , groupPublicKey : groupPublicKey , senderPublicKey : senderPublicKey )
print ( " [Loki] \( error . errorDescription ! ) " )
throw error
}
return ratchet
} else {
var currentKeyIndex = ratchet . keyIndex
var result = ratchet
while currentKeyIndex < targetKeyIndex {
do {
result = try step ( result )
currentKeyIndex = result . keyIndex
} catch {
print ( " [Loki] Couldn't step ratchet due to error: \( error ) . " )
throw error
}
}
Storage . setClosedGroupRatchet ( for : groupPublicKey , senderPublicKey : senderPublicKey , ratchet : result , using : transaction )
return result
}
}
// MARK: P u b l i c A P I
@objc ( encrypt : forGroupWithPublicKey : senderPublicKey : protocolContext : error : )
public func encrypt ( _ plaintext : Data , forGroupWithPublicKey groupPublicKey : String , senderPublicKey : String , protocolContext : Any ) throws -> [ Any ] {
let transaction = protocolContext as ! YapDatabaseReadWriteTransaction
let ( ivAndCiphertext , keyIndex ) = try encrypt ( plaintext , for : groupPublicKey , senderPublicKey : senderPublicKey , using : transaction )
return [ ivAndCiphertext , NSNumber ( value : keyIndex ) ]
}
public func encrypt ( _ plaintext : Data , for groupPublicKey : String , senderPublicKey : String , using transaction : YapDatabaseReadWriteTransaction ) throws -> ( ivAndCiphertext : Data , keyIndex : UInt ) {
let ratchet = try stepRatchetOnce ( for : groupPublicKey , senderPublicKey : senderPublicKey , using : transaction )
let iv = Data . getSecureRandomData ( ofSize : SharedSenderKeysImplementation . ivSize ) !
let gcm = GCM ( iv : iv . bytes , tagLength : Int ( SharedSenderKeysImplementation . gcmTagSize ) , mode : . combined )
let messageKey = ratchet . messageKeys . last !
let aes = try AES ( key : Data ( hex : messageKey ) . bytes , blockMode : gcm , padding : . noPadding )
let ciphertext = try aes . encrypt ( plaintext . bytes )
return ( ivAndCiphertext : iv + Data ( bytes : ciphertext ) , ratchet . keyIndex )
}
@objc ( decrypt : forGroupWithPublicKey : senderPublicKey : keyIndex : protocolContext : error : )
public func decrypt ( _ ivAndCiphertext : Data , forGroupWithPublicKey groupPublicKey : String , senderPublicKey : String , keyIndex : UInt , protocolContext : Any ) throws -> Data {
let transaction = protocolContext as ! YapDatabaseReadWriteTransaction
return try decrypt ( ivAndCiphertext , for : groupPublicKey , senderPublicKey : senderPublicKey , keyIndex : keyIndex , using : transaction )
}
public func decrypt ( _ ivAndCiphertext : Data , for groupPublicKey : String , senderPublicKey : String , keyIndex : UInt , using transaction : YapDatabaseReadWriteTransaction ) throws -> Data {
let ratchet : ClosedGroupRatchet
do {
ratchet = try stepRatchet ( for : groupPublicKey , senderPublicKey : senderPublicKey , until : keyIndex , using : transaction )
} catch {
// FIXME: I t ' d b e c l e a n e r t o h a n d l e t h i s i n O W S M e s s a g e D e c r y p t e r ( w h e r e a l l t h e o t h e r d e c r y p t i o n e r r o r s a r e h a n d l e d ) , b u t t h i s w a s a l o t m o r e
// c o n v e n i e n t b e c a u s e t h e r e ' s a n e a s y w a y t o g e t t h e s e n d e r p u b l i c k e y f r o m h e r e .
if case RatchetingError . loadingFailed ( _ , _ ) = error {
ClosedGroupsProtocol . requestSenderKey ( for : groupPublicKey , senderPublicKey : senderPublicKey , using : transaction )
}
throw error
}
let iv = ivAndCiphertext [ 0. . < Int ( SharedSenderKeysImplementation . ivSize ) ]
let ciphertext = ivAndCiphertext [ Int ( SharedSenderKeysImplementation . ivSize ) . . . ]
let gcm = GCM ( iv : iv . bytes , tagLength : Int ( SharedSenderKeysImplementation . gcmTagSize ) , mode : . combined )
guard let messageKey = ratchet . messageKeys . last else {
throw RatchetingError . messageKeyMissing ( targetKeyIndex : keyIndex , groupPublicKey : groupPublicKey , senderPublicKey : senderPublicKey )
}
let aes = try AES ( key : Data ( hex : messageKey ) . bytes , blockMode : gcm , padding : . noPadding )
return Data ( try aes . decrypt ( ciphertext . bytes ) )
}
public func isClosedGroup ( _ publicKey : String ) -> Bool {
return Storage . getUserClosedGroupPublicKeys ( ) . contains ( publicKey )
}
public func getKeyPair ( forGroupWithPublicKey groupPublicKey : String ) -> ECKeyPair {
let privateKey = Storage . getClosedGroupPrivateKey ( for : groupPublicKey ) !
return ECKeyPair ( publicKey : Data ( hex : groupPublicKey . removing05PrefixIfNeeded ( ) ) , privateKey : Data ( hex : privateKey ) ) !
}
}