2014-11-04 22:59:48 +00:00
/ * v i m : t s = 4 : s w = 4
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Lesser General Public License for more details .
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program . If not , see < http : //www.gnu.org/licenses/>.
* /
; ( function ( ) {
'use strict' ;
window . textsecure = window . textsecure || { } ;
window . textsecure . protocol = function ( ) {
var self = { } ;
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Random constants / utils * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
// We consider messages lost after a week and might throw away keys at that point
// (also the time between signedPreKey regenerations)
var MESSAGE _LOST _THRESHOLD _MS = 1000 * 60 * 60 * 24 * 7 ;
function objectContainsKeys ( object ) {
var count = 0 ;
for ( var key in object ) {
count ++ ;
break ;
}
return count != 0 ;
}
/ * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Key / session storage * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * /
var crypto _storage = { } ;
crypto _storage . putKeyPair = function ( keyName , keyPair ) {
textsecure . storage . putEncrypted ( "25519Key" + keyName , keyPair ) ;
}
crypto _storage . getNewStoredKeyPair = function ( keyName ) {
return textsecure . crypto . createKeyPair ( ) . then ( function ( keyPair ) {
crypto _storage . putKeyPair ( keyName , keyPair ) ;
return keyPair ;
} ) ;
}
crypto _storage . getStoredKeyPair = function ( keyName ) {
var res = textsecure . storage . getEncrypted ( "25519Key" + keyName ) ;
if ( res === undefined )
return undefined ;
return { pubKey : toArrayBuffer ( res . pubKey ) , privKey : toArrayBuffer ( res . privKey ) } ;
}
crypto _storage . removeStoredKeyPair = function ( keyName ) {
textsecure . storage . removeEncrypted ( "25519Key" + keyName ) ;
}
crypto _storage . getIdentityKey = function ( ) {
return this . getStoredKeyPair ( "identityKey" ) ;
}
crypto _storage . saveSession = function ( encodedNumber , session , registrationId ) {
var device = textsecure . storage . devices . getDeviceObject ( encodedNumber ) ;
if ( device === undefined )
device = { sessions : { } , encodedNumber : encodedNumber } ;
if ( registrationId !== undefined )
device . registrationId = registrationId ;
crypto _storage . saveSessionAndDevice ( device , session ) ;
}
crypto _storage . saveSessionAndDevice = function ( device , session ) {
if ( device . sessions === undefined )
device . sessions = { } ;
var sessions = device . sessions ;
var doDeleteSession = false ;
if ( session . indexInfo . closed == - 1 || device . identityKey === undefined )
device . identityKey = session . indexInfo . remoteIdentityKey ;
if ( session . indexInfo . closed != - 1 ) {
doDeleteSession = ( session . indexInfo . closed < ( new Date ( ) . getTime ( ) - MESSAGE _LOST _THRESHOLD _MS ) ) ;
if ( ! doDeleteSession ) {
var keysLeft = false ;
for ( var key in session ) {
if ( key != "indexInfo" && key != "oldRatchetList" && key != "currentRatchet" ) {
keysLeft = true ;
break ;
}
}
doDeleteSession = ! keysLeft ;
console . log ( ( doDeleteSession ? "Deleting " : "Not deleting " ) + "closed session which has not yet timed out" ) ;
} else
console . log ( "Deleting closed session due to timeout (created at " + session . indexInfo . closed + ")" ) ;
}
if ( doDeleteSession )
delete sessions [ getString ( session . indexInfo . baseKey ) ] ;
else
sessions [ getString ( session . indexInfo . baseKey ) ] = session ;
var openSessionRemaining = false ;
for ( var key in sessions )
if ( sessions [ key ] . indexInfo . closed == - 1 )
openSessionRemaining = true ;
if ( ! openSessionRemaining )
try {
delete device [ 'registrationId' ] ;
} catch ( _ ) { }
textsecure . storage . devices . saveDeviceObject ( device ) ;
}
var getSessions = function ( encodedNumber ) {
var device = textsecure . storage . devices . getDeviceObject ( encodedNumber ) ;
if ( device === undefined || device . sessions === undefined )
return undefined ;
return device . sessions ;
}
crypto _storage . getOpenSession = function ( encodedNumber ) {
var sessions = getSessions ( encodedNumber ) ;
if ( sessions === undefined )
return undefined ;
for ( var key in sessions )
if ( sessions [ key ] . indexInfo . closed == - 1 )
return sessions [ key ] ;
return undefined ;
}
crypto _storage . getSessionByRemoteEphemeralKey = function ( encodedNumber , remoteEphemeralKey ) {
var sessions = getSessions ( encodedNumber ) ;
if ( sessions === undefined )
return undefined ;
var searchKey = getString ( remoteEphemeralKey ) ;
var openSession = undefined ;
for ( var key in sessions ) {
if ( sessions [ key ] . indexInfo . closed == - 1 ) {
if ( openSession !== undefined )
throw new Error ( "Datastore inconsistensy: multiple open sessions for " + encodedNumber ) ;
openSession = sessions [ key ] ;
}
if ( sessions [ key ] [ searchKey ] !== undefined )
return sessions [ key ] ;
}
if ( openSession !== undefined )
return openSession ;
return undefined ;
}
crypto _storage . getSessionOrIdentityKeyByBaseKey = function ( encodedNumber , baseKey ) {
var sessions = getSessions ( encodedNumber ) ;
var device = textsecure . storage . devices . getDeviceObject ( encodedNumber ) ;
if ( device === undefined )
return undefined ;
var preferredSession = device . sessions && device . sessions [ getString ( baseKey ) ] ;
if ( preferredSession !== undefined )
return preferredSession ;
if ( device . identityKey !== undefined )
return { indexInfo : { remoteIdentityKey : device . identityKey } } ;
throw new Error ( "Datastore inconsistency: device was stored without identity key" ) ;
}
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Internal Crypto stuff * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
var HKDF = function ( input , salt , info ) {
// HKDF for TextSecure has a bit of additional handling - salts always end up being 32 bytes
if ( salt == '' )
salt = new ArrayBuffer ( 32 ) ;
if ( salt . byteLength != 32 )
throw new Error ( "Got salt of incorrect length" ) ;
info = toArrayBuffer ( info ) ; // TODO: maybe convert calls?
return textsecure . crypto . HKDF ( input , salt , info ) ;
}
var verifyMAC = function ( data , key , mac ) {
return textsecure . crypto . sign ( key , data ) . then ( function ( calculated _mac ) {
if ( ! isEqual ( calculated _mac , mac , true ) )
throw new Error ( "Bad MAC" ) ;
} ) ;
}
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Ratchet implementation * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
var calculateRatchet = function ( session , remoteKey , sending ) {
var ratchet = session . currentRatchet ;
return textsecure . crypto . ECDHE ( remoteKey , toArrayBuffer ( ratchet . ephemeralKeyPair . privKey ) ) . then ( function ( sharedSecret ) {
return HKDF ( sharedSecret , toArrayBuffer ( ratchet . rootKey ) , "WhisperRatchet" ) . then ( function ( masterKey ) {
if ( sending )
session [ getString ( ratchet . ephemeralKeyPair . pubKey ) ] = { messageKeys : { } , chainKey : { counter : - 1 , key : masterKey [ 1 ] } } ;
else
session [ getString ( remoteKey ) ] = { messageKeys : { } , chainKey : { counter : - 1 , key : masterKey [ 1 ] } } ;
ratchet . rootKey = masterKey [ 0 ] ;
} ) ;
} ) ;
}
var initSession = function ( isInitiator , ourEphemeralKey , ourSignedKey , encodedNumber , theirIdentityPubKey , theirEphemeralPubKey , theirSignedPubKey ) {
var ourIdentityKey = crypto _storage . getIdentityKey ( ) ;
if ( isInitiator ) {
if ( ourSignedKey !== undefined )
throw new Error ( "Invalid call to initSession" ) ;
ourSignedKey = ourEphemeralKey ;
} else {
if ( theirSignedPubKey !== undefined )
throw new Error ( "Invalid call to initSession" ) ;
theirSignedPubKey = theirEphemeralPubKey ;
}
var sharedSecret ;
if ( ourEphemeralKey === undefined || theirEphemeralPubKey === undefined )
sharedSecret = new Uint8Array ( 32 * 4 ) ;
else
sharedSecret = new Uint8Array ( 32 * 5 ) ;
for ( var i = 0 ; i < 32 ; i ++ )
sharedSecret [ i ] = 0xff ;
return textsecure . crypto . ECDHE ( theirSignedPubKey , ourIdentityKey . privKey ) . then ( function ( ecRes1 ) {
function finishInit ( ) {
return textsecure . crypto . ECDHE ( theirSignedPubKey , ourSignedKey . privKey ) . then ( function ( ecRes ) {
sharedSecret . set ( new Uint8Array ( ecRes ) , 32 * 3 ) ;
return HKDF ( sharedSecret . buffer , '' , "WhisperText" ) . then ( function ( masterKey ) {
var session = { currentRatchet : { rootKey : masterKey [ 0 ] , lastRemoteEphemeralKey : theirSignedPubKey , previousCounter : 0 } ,
indexInfo : { remoteIdentityKey : theirIdentityPubKey , closed : - 1 } ,
oldRatchetList : [ ]
} ;
if ( ! isInitiator )
session . indexInfo . baseKey = theirEphemeralPubKey ;
else
session . indexInfo . baseKey = ourEphemeralKey . pubKey ;
// If we're initiating we go ahead and set our first sending ephemeral key now,
// otherwise we figure it out when we first maybeStepRatchet with the remote's ephemeral key
if ( isInitiator ) {
return textsecure . crypto . createKeyPair ( ) . then ( function ( ourSendingEphemeralKey ) {
session . currentRatchet . ephemeralKeyPair = ourSendingEphemeralKey ;
return calculateRatchet ( session , theirSignedPubKey , true ) . then ( function ( ) {
return session ;
} ) ;
} ) ;
} else {
session . currentRatchet . ephemeralKeyPair = ourSignedKey ;
return session ;
}
} ) ;
} ) ;
}
var promise ;
if ( ourEphemeralKey === undefined || theirEphemeralPubKey === undefined )
promise = Promise . resolve ( new ArrayBuffer ( 0 ) ) ;
else
promise = textsecure . crypto . ECDHE ( theirEphemeralPubKey , ourEphemeralKey . privKey ) ;
return promise . then ( function ( ecRes4 ) {
sharedSecret . set ( new Uint8Array ( ecRes4 ) , 32 * 4 ) ;
if ( isInitiator )
return textsecure . crypto . ECDHE ( theirIdentityPubKey , ourSignedKey . privKey ) . then ( function ( ecRes2 ) {
sharedSecret . set ( new Uint8Array ( ecRes1 ) , 32 ) ;
sharedSecret . set ( new Uint8Array ( ecRes2 ) , 32 * 2 ) ;
} ) . then ( finishInit ) ;
else
return textsecure . crypto . ECDHE ( theirIdentityPubKey , ourSignedKey . privKey ) . then ( function ( ecRes2 ) {
sharedSecret . set ( new Uint8Array ( ecRes1 ) , 32 * 2 ) ;
sharedSecret . set ( new Uint8Array ( ecRes2 ) , 32 )
} ) . then ( finishInit ) ;
} ) ;
} ) ;
}
var removeOldChains = function ( session ) {
// Sending ratchets are always removed when we step because we never need them again
// Receiving ratchets are either removed if we step with all keys used up to previousCounter
// and are otherwise added to the oldRatchetList, which we parse here and remove ratchets
// older than a week (we assume the message was lost and move on with our lives at that point)
var newList = [ ] ;
for ( var i = 0 ; i < session . oldRatchetList . length ; i ++ ) {
var entry = session . oldRatchetList [ i ] ;
var ratchet = getString ( entry . ephemeralKey ) ;
console . log ( "Checking old chain with added time " + ( entry . added / 1000 ) ) ;
if ( ( ! objectContainsKeys ( session [ ratchet ] . messageKeys ) && ( session [ ratchet ] . chainKey === undefined || session [ ratchet ] . chainKey . key === undefined ) )
|| entry . added < new Date ( ) . getTime ( ) - MESSAGE _LOST _THRESHOLD _MS ) {
delete session [ ratchet ] ;
console . log ( "...deleted" ) ;
} else
newList [ newList . length ] = entry ;
}
session . oldRatchetList = newList ;
}
var closeSession = function ( session , sessionClosedByRemote ) {
if ( session . indexInfo . closed > - 1 )
return ;
// After this has run, we can still receive messages on ratchet chains which
// were already open (unless we know we dont need them),
// but we cannot send messages or step the ratchet
// Delete current sending ratchet
delete session [ getString ( session . currentRatchet . ephemeralKeyPair . pubKey ) ] ;
// Move all receive ratchets to the oldRatchetList to mark them for deletion
for ( var i in session ) {
if ( session [ i ] . chainKey !== undefined && session [ i ] . chainKey . key !== undefined ) {
if ( ! sessionClosedByRemote )
session . oldRatchetList [ session . oldRatchetList . length ] = { added : new Date ( ) . getTime ( ) , ephemeralKey : i } ;
else
delete session [ i ] . chainKey . key ;
}
}
// Delete current root key and our ephemeral key pair to disallow ratchet stepping
delete session . currentRatchet [ 'rootKey' ] ;
delete session . currentRatchet [ 'ephemeralKeyPair' ] ;
session . indexInfo . closed = new Date ( ) . getTime ( ) ;
removeOldChains ( session ) ;
}
self . closeOpenSessionForDevice = function ( encodedNumber ) {
var session = crypto _storage . getOpenSession ( encodedNumber ) ;
if ( session === undefined )
return ;
closeSession ( session ) ;
crypto _storage . saveSession ( encodedNumber , session ) ;
}
var initSessionFromPreKeyWhisperMessage ;
var decryptWhisperMessage ;
var handlePreKeyWhisperMessage = function ( from , encodedMessage ) {
var preKeyProto = textsecure . protobuf . PreKeyWhisperMessage . decode ( encodedMessage , 'binary' ) ;
return initSessionFromPreKeyWhisperMessage ( from , preKeyProto ) . then ( function ( sessions ) {
return decryptWhisperMessage ( from , getString ( preKeyProto . message ) , sessions [ 0 ] , preKeyProto . registrationId ) . then ( function ( result ) {
if ( sessions [ 1 ] !== undefined )
sessions [ 1 ] ( ) ;
return result ;
} ) ;
} ) ;
}
var wipeIdentityAndTryMessageAgain = function ( from , encodedMessage ) {
//TODO: Wipe identity key!
return handlePreKeyWhisperMessage ( from , encodedMessage ) ;
}
textsecure . replay . registerReplayFunction ( wipeIdentityAndTryMessageAgain , textsecure . replay . REPLAY _FUNCS . INIT _SESSION ) ;
initSessionFromPreKeyWhisperMessage = function ( encodedNumber , message ) {
var preKeyPair = crypto _storage . getStoredKeyPair ( "preKey" + message . preKeyId ) ;
var signedPreKeyPair = crypto _storage . getStoredKeyPair ( "signedKey" + message . signedPreKeyId ) ;
var session = crypto _storage . getSessionOrIdentityKeyByBaseKey ( encodedNumber , toArrayBuffer ( message . baseKey ) ) ;
var open _session = crypto _storage . getOpenSession ( encodedNumber ) ;
if ( signedPreKeyPair === undefined ) {
// Session may or may not be the right one, but if its not, we can't do anything about it
// ...fall through and let decryptWhisperMessage handle that case
if ( session !== undefined && session . currentRatchet !== undefined )
return Promise . resolve ( [ session , undefined ] ) ;
else
throw new Error ( "Missing Signed PreKey for PreKeyWhisperMessage" ) ;
}
if ( session !== undefined ) {
// Duplicate PreKeyMessage for session:
if ( isEqual ( session . indexInfo . baseKey , message . baseKey , false ) )
return Promise . resolve ( [ session , undefined ] ) ;
// We already had a session/known identity key:
if ( isEqual ( session . indexInfo . remoteIdentityKey , message . identityKey , false ) ) {
// If the identity key matches the previous one, close the previous one and use the new one
if ( open _session !== undefined )
closeSession ( open _session ) ; // To be returned and saved later
} else {
// ...otherwise create an error that the UI will pick up and ask the user if they want to re-negotiate
throw new Error ( "Received message with unknown identity key" , "The identity of the sender has changed. This may be malicious, or the sender may have simply reinstalled TextSecure." , textsecure . replay . REPLAY _FUNCS . INIT _SESSION , [ encodedNumber , getString ( message . encode ( ) ) ] ) ;
}
}
return initSession ( false , preKeyPair , signedPreKeyPair , encodedNumber , toArrayBuffer ( message . identityKey ) , toArrayBuffer ( message . baseKey ) , undefined )
. then ( function ( new _session ) {
// Note that the session is not actually saved until the very end of decryptWhisperMessage
// ... to ensure that the sender actually holds the private keys for all reported pubkeys
return [ new _session , function ( ) {
if ( open _session !== undefined )
crypto _storage . saveSession ( encodedNumber , open _session ) ;
crypto _storage . removeStoredKeyPair ( "preKey" + message . preKeyId ) ;
} ] ;
} ) ; ;
}
var fillMessageKeys = function ( chain , counter ) {
if ( chain . chainKey . counter + 1000 < counter ) //TODO: maybe 1000 is too low/high in some cases?
return Promise . resolve ( ) ; // Stalker, much?
if ( chain . chainKey . counter >= counter )
return Promise . resolve ( ) ; // Already calculated
if ( chain . chainKey . key === undefined )
throw new Error ( "Got invalid request to extend chain after it was already closed" ) ;
var key = toArrayBuffer ( chain . chainKey . key ) ;
var byteArray = new Uint8Array ( 1 ) ;
byteArray [ 0 ] = 1 ;
return textsecure . crypto . sign ( key , byteArray . buffer ) . then ( function ( mac ) {
byteArray [ 0 ] = 2 ;
return textsecure . crypto . sign ( key , byteArray . buffer ) . then ( function ( key ) {
chain . messageKeys [ chain . chainKey . counter + 1 ] = mac ;
chain . chainKey . key = key
chain . chainKey . counter += 1 ;
return fillMessageKeys ( chain , counter ) ;
} ) ;
} ) ;
}
var maybeStepRatchet = function ( session , remoteKey , previousCounter ) {
if ( session [ getString ( remoteKey ) ] !== undefined )
return Promise . resolve ( ) ;
var ratchet = session . currentRatchet ;
var finish = function ( ) {
return calculateRatchet ( session , remoteKey , false ) . then ( function ( ) {
// Now swap the ephemeral key and calculate the new sending chain
var previousRatchet = getString ( ratchet . ephemeralKeyPair . pubKey ) ;
if ( session [ previousRatchet ] !== undefined ) {
ratchet . previousCounter = session [ previousRatchet ] . chainKey . counter ;
delete session [ previousRatchet ] ;
}
return textsecure . crypto . createKeyPair ( ) . then ( function ( keyPair ) {
ratchet . ephemeralKeyPair = keyPair ;
return calculateRatchet ( session , remoteKey , true ) . then ( function ( ) {
ratchet . lastRemoteEphemeralKey = remoteKey ;
} ) ;
} ) ;
} ) ;
}
var previousRatchet = session [ getString ( ratchet . lastRemoteEphemeralKey ) ] ;
if ( previousRatchet !== undefined ) {
return fillMessageKeys ( previousRatchet , previousCounter ) . then ( function ( ) {
delete previousRatchet . chainKey . key ;
if ( ! objectContainsKeys ( previousRatchet . messageKeys ) )
delete session [ getString ( ratchet . lastRemoteEphemeralKey ) ] ;
else
session . oldRatchetList [ session . oldRatchetList . length ] = { added : new Date ( ) . getTime ( ) , ephemeralKey : ratchet . lastRemoteEphemeralKey } ;
} ) . then ( finish ) ;
} else
return finish ( ) ;
}
// returns decrypted protobuf
decryptWhisperMessage = function ( encodedNumber , messageBytes , session , registrationId ) {
if ( messageBytes [ 0 ] != String . fromCharCode ( ( 3 << 4 ) | 3 ) )
throw new Error ( "Bad version number on WhisperMessage" ) ;
var messageProto = messageBytes . substring ( 1 , messageBytes . length - 8 ) ;
var mac = messageBytes . substring ( messageBytes . length - 8 , messageBytes . length ) ;
var message = textsecure . protobuf . WhisperMessage . decode ( messageProto , 'binary' ) ;
var remoteEphemeralKey = toArrayBuffer ( message . ephemeralKey ) ;
if ( session === undefined ) {
var session = crypto _storage . getSessionByRemoteEphemeralKey ( encodedNumber , remoteEphemeralKey ) ;
if ( session === undefined )
throw new Error ( "No session found to decrypt message from " + encodedNumber ) ;
}
return maybeStepRatchet ( session , remoteEphemeralKey , message . previousCounter ) . then ( function ( ) {
var chain = session [ getString ( message . ephemeralKey ) ] ;
return fillMessageKeys ( chain , message . counter ) . then ( function ( ) {
return HKDF ( toArrayBuffer ( chain . messageKeys [ message . counter ] ) , '' , "WhisperMessageKeys" ) . then ( function ( keys ) {
delete chain . messageKeys [ message . counter ] ;
var messageProtoArray = toArrayBuffer ( messageProto ) ;
var macInput = new Uint8Array ( messageProtoArray . byteLength + 33 * 2 + 1 ) ;
macInput . set ( new Uint8Array ( toArrayBuffer ( session . indexInfo . remoteIdentityKey ) ) ) ;
macInput . set ( new Uint8Array ( toArrayBuffer ( crypto _storage . getIdentityKey ( ) . pubKey ) ) , 33 ) ;
macInput [ 33 * 2 ] = ( 3 << 4 ) | 3 ;
macInput . set ( new Uint8Array ( messageProtoArray ) , 33 * 2 + 1 ) ;
return verifyMAC ( macInput . buffer , keys [ 1 ] , mac ) . then ( function ( ) {
return window . textsecure . crypto . decrypt ( keys [ 0 ] , toArrayBuffer ( message . ciphertext ) , keys [ 2 ] . slice ( 0 , 16 ) )
. then ( function ( paddedPlaintext ) {
paddedPlaintext = new Uint8Array ( paddedPlaintext ) ;
var plaintext ;
for ( var i = paddedPlaintext . length - 1 ; i >= 0 ; i -- ) {
if ( paddedPlaintext [ i ] == 0x80 ) {
plaintext = new Uint8Array ( i ) ;
plaintext . set ( paddedPlaintext . subarray ( 0 , i ) ) ;
plaintext = plaintext . buffer ;
break ;
} else if ( paddedPlaintext [ i ] != 0x00 )
throw new Error ( 'Invalid padding' ) ;
}
delete session [ 'pendingPreKey' ] ;
var finalMessage = textsecure . protobuf . PushMessageContent . decode ( plaintext ) ;
if ( ( finalMessage . flags & textsecure . protobuf . PushMessageContent . Flags . END _SESSION )
== textsecure . protobuf . PushMessageContent . Flags . END _SESSION )
closeSession ( session , true ) ;
removeOldChains ( session ) ;
crypto _storage . saveSession ( encodedNumber , session , registrationId ) ;
return finalMessage ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
}
/ * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Public crypto API * * *
* * * * * * * * * * * * * * * * * * * * * * * * * /
// Decrypts message into a raw string
self . decryptWebsocketMessage = function ( message ) {
var signaling _key = textsecure . storage . getEncrypted ( "signaling_key" ) ; //TODO: in crypto_storage
var aes _key = toArrayBuffer ( signaling _key . substring ( 0 , 32 ) ) ;
var mac _key = toArrayBuffer ( signaling _key . substring ( 32 , 32 + 20 ) ) ;
var decodedMessage = base64DecToArr ( getString ( message ) ) ;
if ( new Uint8Array ( decodedMessage ) [ 0 ] != 1 )
throw new Error ( "Got bad version number: " + decodedMessage [ 0 ] ) ;
var iv = decodedMessage . slice ( 1 , 1 + 16 ) ;
var ciphertext = decodedMessage . slice ( 1 + 16 , decodedMessage . byteLength - 10 ) ;
var ivAndCiphertext = decodedMessage . slice ( 0 , decodedMessage . byteLength - 10 ) ;
var mac = decodedMessage . slice ( decodedMessage . byteLength - 10 , decodedMessage . byteLength ) ;
return verifyMAC ( ivAndCiphertext , mac _key , mac ) . then ( function ( ) {
return window . textsecure . crypto . decrypt ( aes _key , ciphertext , iv ) ;
} ) ;
} ;
self . decryptAttachment = function ( encryptedBin , keys ) {
var aes _key = keys . slice ( 0 , 32 ) ;
var mac _key = keys . slice ( 32 , 64 ) ;
var iv = encryptedBin . slice ( 0 , 16 ) ;
var ciphertext = encryptedBin . slice ( 16 , encryptedBin . byteLength - 32 ) ;
var ivAndCiphertext = encryptedBin . slice ( 0 , encryptedBin . byteLength - 32 ) ;
var mac = encryptedBin . slice ( encryptedBin . byteLength - 32 , encryptedBin . byteLength ) ;
return verifyMAC ( ivAndCiphertext , mac _key , mac ) . then ( function ( ) {
return window . textsecure . crypto . decrypt ( aes _key , ciphertext , iv ) ;
} ) ;
} ;
self . encryptAttachment = function ( plaintext , keys , iv ) {
var aes _key = keys . slice ( 0 , 32 ) ;
var mac _key = keys . slice ( 32 , 64 ) ;
return window . textsecure . crypto . encrypt ( aes _key , plaintext , iv ) . then ( function ( ciphertext ) {
var ivAndCiphertext = new Uint8Array ( 16 + ciphertext . byteLength ) ;
ivAndCiphertext . set ( new Uint8Array ( iv ) ) ;
ivAndCiphertext . set ( new Uint8Array ( ciphertext ) , 16 ) ;
return textsecure . crypto . sign ( mac _key , ivAndCiphertext . buffer ) . then ( function ( mac ) {
var encryptedBin = new Uint8Array ( 16 + ciphertext . byteLength + 32 ) ;
encryptedBin . set ( ivAndCiphertext ) ;
encryptedBin . set ( new Uint8Array ( mac ) , 16 + ciphertext . byteLength ) ;
return encryptedBin . buffer ;
} ) ;
} ) ;
} ;
self . handleIncomingPushMessageProto = function ( proto ) {
switch ( proto . type ) {
case textsecure . protobuf . IncomingPushMessageSignal . Type . PLAINTEXT :
return Promise . resolve ( textsecure . protobuf . PushMessageContent . decode ( proto . message ) ) ;
case textsecure . protobuf . IncomingPushMessageSignal . Type . CIPHERTEXT :
var from = proto . source + "." + ( proto . sourceDevice == null ? 0 : proto . sourceDevice ) ;
return decryptWhisperMessage ( from , getString ( proto . message ) ) ;
case textsecure . protobuf . IncomingPushMessageSignal . Type . PREKEY _BUNDLE :
if ( proto . message . readUint8 ( ) != ( ( 3 << 4 ) | 3 ) )
throw new Error ( "Bad version byte" ) ;
var from = proto . source + "." + ( proto . sourceDevice == null ? 0 : proto . sourceDevice ) ;
return handlePreKeyWhisperMessage ( from , getString ( proto . message ) ) ;
case textsecure . protobuf . IncomingPushMessageSignal . Type . RECEIPT :
return Promise . resolve ( null ) ;
default :
return new Promise ( function ( resolve , reject ) { reject ( new Error ( "Unknown message type" ) ) ; } ) ;
}
}
// return Promise(encoded [PreKey]WhisperMessage)
self . encryptMessageFor = function ( deviceObject , pushMessageContent ) {
var session = crypto _storage . getOpenSession ( deviceObject . encodedNumber ) ;
var doEncryptPushMessageContent = function ( ) {
var msg = new textsecure . protobuf . WhisperMessage ( ) ;
var plaintext = toArrayBuffer ( pushMessageContent . encode ( ) ) ;
var paddedPlaintext = new Uint8Array ( Math . ceil ( ( plaintext . byteLength + 1 ) / 160.0 ) * 160 ) ;
paddedPlaintext . set ( new Uint8Array ( plaintext ) ) ;
paddedPlaintext [ plaintext . byteLength ] = 0x80 ;
msg . ephemeralKey = toArrayBuffer ( session . currentRatchet . ephemeralKeyPair . pubKey ) ;
var chain = session [ getString ( msg . ephemeralKey ) ] ;
return fillMessageKeys ( chain , chain . chainKey . counter + 1 ) . then ( function ( ) {
return HKDF ( toArrayBuffer ( chain . messageKeys [ chain . chainKey . counter ] ) , '' , "WhisperMessageKeys" ) . then ( function ( keys ) {
delete chain . messageKeys [ chain . chainKey . counter ] ;
msg . counter = chain . chainKey . counter ;
msg . previousCounter = session . currentRatchet . previousCounter ;
return window . textsecure . crypto . encrypt ( keys [ 0 ] , paddedPlaintext . buffer , keys [ 2 ] . slice ( 0 , 16 ) ) . then ( function ( ciphertext ) {
msg . ciphertext = ciphertext ;
var encodedMsg = toArrayBuffer ( msg . encode ( ) ) ;
var macInput = new Uint8Array ( encodedMsg . byteLength + 33 * 2 + 1 ) ;
macInput . set ( new Uint8Array ( toArrayBuffer ( crypto _storage . getIdentityKey ( ) . pubKey ) ) ) ;
macInput . set ( new Uint8Array ( toArrayBuffer ( session . indexInfo . remoteIdentityKey ) ) , 33 ) ;
macInput [ 33 * 2 ] = ( 3 << 4 ) | 3 ;
macInput . set ( new Uint8Array ( encodedMsg ) , 33 * 2 + 1 ) ;
return textsecure . crypto . sign ( keys [ 1 ] , macInput . buffer ) . then ( function ( mac ) {
var result = new Uint8Array ( encodedMsg . byteLength + 9 ) ;
result [ 0 ] = ( 3 << 4 ) | 3 ;
result . set ( new Uint8Array ( encodedMsg ) , 1 ) ;
result . set ( new Uint8Array ( mac , 0 , 8 ) , encodedMsg . byteLength + 1 ) ;
try {
delete deviceObject [ 'signedKey' ] ;
delete deviceObject [ 'signedKeyId' ] ;
delete deviceObject [ 'preKey' ] ;
delete deviceObject [ 'preKeyId' ] ;
} catch ( _ ) { }
removeOldChains ( session ) ;
crypto _storage . saveSessionAndDevice ( deviceObject , session ) ;
return result ;
} ) ;
} ) ;
} ) ;
} ) ;
}
var preKeyMsg = new textsecure . protobuf . PreKeyWhisperMessage ( ) ;
preKeyMsg . identityKey = toArrayBuffer ( crypto _storage . getIdentityKey ( ) . pubKey ) ;
preKeyMsg . registrationId = textsecure . storage . getUnencrypted ( "registrationId" ) ;
if ( session === undefined ) {
return textsecure . crypto . createKeyPair ( ) . then ( function ( baseKey ) {
preKeyMsg . preKeyId = deviceObject . preKeyId ;
preKeyMsg . signedPreKeyId = deviceObject . signedKeyId ;
preKeyMsg . baseKey = toArrayBuffer ( baseKey . pubKey ) ;
return initSession ( true , baseKey , undefined , deviceObject . encodedNumber ,
toArrayBuffer ( deviceObject . identityKey ) , toArrayBuffer ( deviceObject . preKey ) , toArrayBuffer ( deviceObject . signedKey ) )
. then ( function ( new _session ) {
session = new _session ;
session . pendingPreKey = { preKeyId : deviceObject . preKeyId , signedKeyId : deviceObject . signedKeyId , baseKey : baseKey . pubKey } ;
return doEncryptPushMessageContent ( ) . then ( function ( message ) {
preKeyMsg . message = message ;
var result = String . fromCharCode ( ( 3 << 4 ) | 3 ) + getString ( preKeyMsg . encode ( ) ) ;
return { type : 3 , body : result } ;
} ) ;
} ) ;
} ) ;
} else
return doEncryptPushMessageContent ( ) . then ( function ( message ) {
if ( session . pendingPreKey !== undefined ) {
preKeyMsg . baseKey = toArrayBuffer ( session . pendingPreKey . baseKey ) ;
preKeyMsg . preKeyId = session . pendingPreKey . preKeyId ;
preKeyMsg . signedPreKeyId = session . pendingPreKey . signedKeyId ;
preKeyMsg . message = message ;
var result = String . fromCharCode ( ( 3 << 4 ) | 3 ) + getString ( preKeyMsg . encode ( ) ) ;
return { type : 3 , body : result } ;
} else
return { type : 1 , body : getString ( message ) } ;
} ) ;
}
var GENERATE _KEYS _KEYS _GENERATED = 100 ;
self . generateKeys = function ( ) {
var identityKeyPair = crypto _storage . getIdentityKey ( ) ;
var identityKeyCalculated = function ( identityKeyPair ) {
var firstPreKeyId = textsecure . storage . getEncrypted ( "maxPreKeyId" , 0 ) ;
textsecure . storage . putEncrypted ( "maxPreKeyId" , firstPreKeyId + GENERATE _KEYS _KEYS _GENERATED ) ;
var signedKeyId = textsecure . storage . getEncrypted ( "signedKeyId" , 0 ) ;
textsecure . storage . putEncrypted ( "signedKeyId" , signedKeyId + 1 ) ;
var keys = { } ;
keys . identityKey = identityKeyPair . pubKey ;
keys . preKeys = [ ] ;
var generateKey = function ( keyId ) {
return crypto _storage . getNewStoredKeyPair ( "preKey" + keyId , false ) . then ( function ( keyPair ) {
keys . preKeys [ keyId ] = { keyId : keyId , publicKey : keyPair . pubKey } ;
} ) ;
} ;
var promises = [ ] ;
for ( var i = firstPreKeyId ; i < firstPreKeyId + GENERATE _KEYS _KEYS _GENERATED ; i ++ )
promises [ i ] = generateKey ( i ) ;
promises [ firstPreKeyId + GENERATE _KEYS _KEYS _GENERATED ] = crypto _storage . getNewStoredKeyPair ( "signedKey" + signedKeyId ) . then ( function ( keyPair ) {
return textsecure . crypto . Ed25519Sign ( identityKeyPair . privKey , keyPair . pubKey ) . then ( function ( sig ) {
keys . signedPreKey = { keyId : signedKeyId , publicKey : keyPair . pubKey , signature : sig } ;
} ) ;
} ) ;
//TODO: Process by date added and agressively call generateKeys when we get near maxPreKeyId in a message
crypto _storage . removeStoredKeyPair ( "signedKey" + ( signedKeyId - 2 ) ) ;
return Promise . all ( promises ) . then ( function ( ) {
return keys ;
} ) ;
}
if ( identityKeyPair === undefined )
return crypto _storage . getNewStoredKeyPair ( "identityKey" , true ) . then ( function ( keyPair ) { return identityKeyCalculated ( keyPair ) ; } ) ;
else
return identityKeyCalculated ( identityKeyPair ) ;
}
Finish abstracting native client
Firstly, don't initialize textsecure.nativclient unless the browser
supports it. The mimetype-check trick is hewn from nacl-common.js.
Secondly, nativeclient crypto functions will all automatically wait for
the module to load before sending messages, so we needn't register any
onload callbacks outside nativeclient.js. (Previously, if you wanted to
do crypto with native client, you would have to register a call back and
wait for the module to load.) Now that the native client crypto is
encapsulated behind a nice interface, it can handle all that
onload-callback jazz internally: if the module isn't loaded when you
call a nativeclient function, return a promise that waits for the load
callback, and eventually resolves with the result of the requested
command. This removes the need for textsecure.registerOnLoadCallback.
Finally, although native client has its quirks, it's significantly
faster than the alternative (emscripten compiled js), so this commit
also lets the crypto backend use native client opportunistically, if
it's available, falling back to js if not, which should make us
compatible with older versions of chrome and chromium.
2014-11-09 01:26:20 +00:00
//TODO: Dont always update prekeys here
if ( textsecure . storage . getEncrypted ( "lastSignedKeyUpdate" , Date . now ( ) ) < Date . now ( ) - MESSAGE _LOST _THRESHOLD _MS ) {
new Promise ( function ( resolve ) { resolve ( self . generateKeys ( ) ) ; } ) ;
}
2014-11-04 22:59:48 +00:00
self . prepareTempWebsocket = function ( ) {
var socketInfo = { } ;
var keyPair ;
socketInfo . decryptAndHandleDeviceInit = function ( deviceInit ) {
var masterEphemeral = toArrayBuffer ( deviceInit . masterEphemeralPubKey ) ;
var message = toArrayBuffer ( deviceInit . identityKeyMessage ) ;
return textsecure . crypto . ECDHE ( masterEphemeral , keyPair . privKey ) . then ( function ( ecRes ) {
return HKDF ( ecRes , masterEphemeral , "WhisperDeviceInit" ) . then ( function ( keys ) {
if ( new Uint8Array ( message ) [ 0 ] != ( 3 << 4 ) | 3 )
throw new Error ( "Bad version number on IdentityKeyMessage" ) ;
var iv = message . slice ( 1 , 16 + 1 ) ;
var mac = message . slice ( message . length - 32 , message . length ) ;
var ivAndCiphertext = message . slice ( 0 , message . length - 32 ) ;
var ciphertext = message . slice ( 16 + 1 , message . length - 32 ) ;
return verifyMAC ( ivAndCiphertext , ecRes [ 1 ] , mac ) . then ( function ( ) {
window . textsecure . crypto . decrypt ( ecRes [ 0 ] , ciphertext , iv ) . then ( function ( plaintext ) {
var identityKeyMsg = textsecure . protobuf . IdentityKey . decode ( plaintext ) ;
textsecure . crypto . createKeyPair ( toArrayBuffer ( identityKeyMsg . identityKey ) ) . then ( function ( identityKeyPair ) {
crypto _storage . putKeyPair ( "identityKey" , identityKeyPair ) ;
identityKeyMsg . identityKey = null ;
return identityKeyMsg ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
}
return textsecure . crypto . createKeyPair ( ) . then ( function ( newKeyPair ) {
keyPair = newKeyPair ;
socketInfo . pubKey = keyPair . pubKey ;
return socketInfo ;
} ) ;
}
return self ;
} ( ) ;
} ) ( ) ;