2014-05-14 20:58:12 +02: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/>.
* /
2014-05-17 07:53:58 +02:00
window . textsecure = window . textsecure || { } ;
2014-05-09 08:00:49 +02:00
2014-05-28 03:53:43 +02:00
window . textsecure . crypto = function ( ) {
2014-06-10 02:14:52 +02:00
'use strict' ;
2014-05-17 07:53:58 +02:00
var self = { } ;
2014-05-17 06:54:12 +02:00
// functions exposed for replacement and direct calling in test code
var testing _only = { } ;
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Random constants / utils * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
2014-05-14 11:10:05 +02:00
// We consider messages lost after a week and might throw away keys at that point
var MESSAGE _LOST _THRESHOLD _MS = 1000 * 60 * 60 * 24 * 7 ;
2014-05-17 06:54:12 +02:00
var getRandomBytes = function ( size ) {
2014-06-10 02:14:52 +02:00
// At some point we might consider XORing in hashes of random
// UI events to strengthen ourselves against RNG flaws in crypto.getRandomValues
// ie maybe take a look at how Gibson does it at https://www.grc.com/r&d/js.htm
var array = new Uint8Array ( size ) ;
window . crypto . getRandomValues ( array ) ;
return array . buffer ;
2014-05-09 08:00:49 +02:00
}
2014-05-17 07:53:58 +02:00
self . getRandomBytes = getRandomBytes ;
2014-05-17 06:54:12 +02:00
function intToArrayBuffer ( nInt ) {
var res = new ArrayBuffer ( 16 ) ;
var thing = new Uint8Array ( res ) ;
thing [ 0 ] = ( nInt >> 24 ) & 0xff ;
thing [ 1 ] = ( nInt >> 16 ) & 0xff ;
thing [ 2 ] = ( nInt >> 8 ) & 0xff ;
thing [ 3 ] = ( nInt >> 0 ) & 0xff ;
return res ;
}
2014-05-09 08:00:49 +02:00
2014-05-21 04:21:07 +02:00
function objectContainsKeys ( object ) {
var count = 0 ;
for ( key in object ) {
count ++ ;
break ;
}
return count != 0 ;
}
2014-05-09 09:43:23 +02:00
function HmacSHA256 ( key , input ) {
return window . crypto . subtle . sign ( { name : "HMAC" , hash : "SHA-256" } , key , input ) ;
}
2014-05-17 06:54:12 +02:00
testing _only . privToPub = function ( privKey , isIdentity ) {
2014-05-09 08:00:49 +02:00
if ( privKey . byteLength != 32 )
throw new Error ( "Invalid private key" ) ;
var prependVersion = function ( pubKey ) {
var origPub = new Uint8Array ( pubKey ) ;
var pub = new ArrayBuffer ( 33 ) ;
var pubWithPrefix = new Uint8Array ( pub ) ;
2014-05-13 07:51:46 +02:00
pubWithPrefix . set ( origPub , 1 ) ;
2014-05-09 08:00:49 +02:00
pubWithPrefix [ 0 ] = 5 ;
return pub ;
}
2014-05-17 07:53:58 +02:00
if ( textsecure . nacl . USE _NACL ) {
2014-05-17 07:55:32 +02:00
return textsecure . nacl . postNaclMessage ( { command : "bytesToPriv" , priv : privKey } ) . then ( function ( message ) {
2014-05-09 09:20:54 +02:00
var priv = message . res ;
if ( ! isIdentity )
new Uint8Array ( priv ) [ 0 ] |= 0x01 ;
2014-05-17 07:55:32 +02:00
return textsecure . nacl . postNaclMessage ( { command : "privToPub" , priv : priv } ) . then ( function ( message ) {
2014-05-09 09:20:54 +02:00
return { pubKey : prependVersion ( message . res ) , privKey : priv } ;
2014-05-09 08:00:49 +02:00
} ) ;
} ) ;
} else {
privKey = privKey . slice ( 0 ) ;
var priv = new Uint16Array ( privKey ) ;
priv [ 0 ] &= 0xFFF8 ;
priv [ 15 ] = ( priv [ 15 ] & 0x7FFF ) | 0x4000 ;
if ( ! isIdentity )
priv [ 0 ] |= 0x0001 ;
//TODO: fscking type conversion
2014-05-18 21:58:53 +02:00
return Promise . resolve ( { pubKey : prependVersion ( toArrayBuffer ( curve25519 ( priv ) ) ) , privKey : privKey } ) ;
2014-05-09 08:00:49 +02:00
}
}
2014-05-17 06:54:12 +02:00
var privToPub = function ( privKey , isIdentity ) { return testing _only . privToPub ( privKey , isIdentity ) ; }
2014-05-09 08:00:49 +02:00
2014-05-17 06:54:12 +02:00
testing _only . createNewKeyPair = function ( isIdentity ) {
return privToPub ( getRandomBytes ( 32 ) , isIdentity ) ;
2014-05-09 08:00:49 +02:00
}
2014-05-17 06:54:12 +02:00
var createNewKeyPair = function ( isIdentity ) { return testing _only . createNewKeyPair ( isIdentity ) ; }
2014-05-09 08:00:49 +02:00
2014-05-17 06:54:12 +02:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Key / session storage * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * /
2014-05-09 08:00:49 +02:00
var crypto _storage = { } ;
crypto _storage . getNewPubKeySTORINGPrivKey = function ( keyName , isIdentity ) {
return createNewKeyPair ( isIdentity ) . then ( function ( keyPair ) {
2014-05-21 04:21:07 +02:00
textsecure . storage . putEncrypted ( "25519Key" + keyName , keyPair ) ;
2014-05-09 08:00:49 +02:00
return keyPair . pubKey ;
} ) ;
}
crypto _storage . getStoredPubKey = function ( keyName ) {
2014-05-21 04:21:07 +02:00
return toArrayBuffer ( textsecure . storage . getEncrypted ( "25519Key" + keyName , { pubKey : undefined } ) . pubKey ) ;
2014-05-09 08:00:49 +02:00
}
crypto _storage . getStoredKeyPair = function ( keyName ) {
2014-05-21 04:21:07 +02:00
var res = textsecure . storage . getEncrypted ( "25519Key" + keyName ) ;
2014-05-09 08:00:49 +02:00
if ( res === undefined )
return undefined ;
return { pubKey : toArrayBuffer ( res . pubKey ) , privKey : toArrayBuffer ( res . privKey ) } ;
}
crypto _storage . getAndRemoveStoredKeyPair = function ( keyName ) {
var keyPair = this . getStoredKeyPair ( keyName ) ;
2014-05-21 04:21:07 +02:00
textsecure . storage . removeEncrypted ( "25519Key" + keyName ) ;
2014-05-09 08:00:49 +02:00
return keyPair ;
}
crypto _storage . getAndRemovePreKeyPair = function ( keyId ) {
return this . getAndRemoveStoredKeyPair ( "preKey" + keyId ) ;
}
crypto _storage . getIdentityPrivKey = function ( ) {
return this . getStoredKeyPair ( "identityKey" ) . privKey ;
}
2014-05-31 19:28:46 +02:00
crypto _storage . saveSession = function ( encodedNumber , session , registrationId ) {
var device = textsecure . storage . devices . getDeviceObject ( encodedNumber ) ;
if ( device === undefined )
device = { sessions : { } , encodedNumber : encodedNumber } ;
if ( device . sessions === undefined )
device . sessions = { } ;
if ( registrationId !== undefined )
device . registrationId = registrationId ;
var sessions = device . sessions ;
2014-05-14 11:10:05 +02:00
var doDeleteSession = false ;
if ( session . indexInfo . closed == - 1 )
2014-05-31 19:28:46 +02:00
device . identityKey = session . indexInfo . remoteIdentityKey ;
2014-05-14 11:10:05 +02:00
else {
doDeleteSession = ( session . indexInfo . closed < ( new Date ( ) . getTime ( ) - MESSAGE _LOST _THRESHOLD _MS ) ) ;
if ( ! doDeleteSession ) {
var keysLeft = false ;
for ( key in session ) {
if ( key != "indexInfo" && key != "indexInfo" && key != "oldRatchetList" ) {
keysLeft = true ;
break ;
}
}
doDeleteSession = ! keysLeft ;
}
}
2014-05-09 08:00:49 +02:00
2014-05-14 11:10:05 +02:00
if ( doDeleteSession )
delete sessions [ getString ( session . indexInfo . baseKey ) ] ;
else
sessions [ getString ( session . indexInfo . baseKey ) ] = session ;
2014-05-31 19:28:46 +02:00
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 ;
2014-05-09 08:00:49 +02:00
}
2014-05-14 23:20:49 +02:00
crypto _storage . getOpenSession = function ( encodedNumber ) {
2014-05-31 19:28:46 +02:00
var sessions = getSessions ( encodedNumber ) ;
2014-05-14 11:10:05 +02:00
if ( sessions === undefined )
return undefined ;
2014-05-31 19:28:46 +02:00
for ( key in sessions )
2014-05-14 23:20:49 +02:00
if ( sessions [ key ] . indexInfo . closed == - 1 )
return sessions [ key ] ;
return undefined ;
}
crypto _storage . getSessionByRemoteEphemeralKey = function ( encodedNumber , remoteEphemeralKey ) {
2014-05-31 19:28:46 +02:00
var sessions = getSessions ( encodedNumber ) ;
2014-05-14 23:20:49 +02:00
if ( sessions === undefined )
return undefined ;
var searchKey = getString ( remoteEphemeralKey ) ;
2014-05-14 11:10:05 +02:00
var openSession = undefined ;
for ( 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 ;
2014-05-14 23:20:49 +02:00
return undefined ;
}
crypto _storage . getSessionOrIdentityKeyByBaseKey = function ( encodedNumber , baseKey ) {
2014-05-31 19:28:46 +02:00
var sessions = getSessions ( encodedNumber ) ;
var device = textsecure . storage . devices . getDeviceObject ( encodedNumber ) ;
if ( device === undefined )
2014-05-14 23:20:49 +02:00
return undefined ;
2014-05-31 19:28:46 +02:00
var preferredSession = device . sessions && device . sessions [ getString ( baseKey ) ] ;
2014-05-14 23:20:49 +02:00
if ( preferredSession !== undefined )
return preferredSession ;
2014-05-31 19:28:46 +02:00
if ( device . identityKey !== undefined )
return { indexInfo : { remoteIdentityKey : device . identityKey } } ;
2014-05-14 11:10:05 +02:00
2014-05-31 19:28:46 +02:00
throw new Error ( "Datastore inconsistency: device was stored without identity key" ) ;
2014-05-14 11:10:05 +02:00
}
2014-05-09 08:00:49 +02:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Internal Crypto stuff * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
2014-05-17 06:54:12 +02:00
testing _only . ECDHE = function ( pubKey , privKey ) {
2014-05-13 07:51:46 +02:00
if ( privKey === undefined || privKey . byteLength != 32 )
2014-05-09 08:00:49 +02:00
throw new Error ( "Invalid private key" ) ;
2014-05-13 10:40:29 +02:00
if ( pubKey === undefined || ( ( pubKey . byteLength != 33 || new Uint8Array ( pubKey ) [ 0 ] != 5 ) && pubKey . byteLength != 32 ) )
2014-05-13 07:51:46 +02:00
throw new Error ( "Invalid public key" ) ;
2014-05-13 10:40:29 +02:00
if ( pubKey . byteLength == 33 )
pubKey = pubKey . slice ( 1 ) ;
else
console . error ( "WARNING: Expected pubkey of length 33, please report the ST and client that generated the pubkey" ) ;
2014-05-09 08:00:49 +02:00
return new Promise ( function ( resolve ) {
2014-05-17 07:53:58 +02:00
if ( textsecure . nacl . USE _NACL ) {
2014-05-17 07:55:32 +02:00
textsecure . nacl . postNaclMessage ( { command : "ECDHE" , priv : privKey , pub : pubKey } ) . then ( function ( message ) {
2014-05-09 08:00:49 +02:00
resolve ( message . res ) ;
} ) ;
} else {
2014-05-13 10:40:29 +02:00
resolve ( toArrayBuffer ( curve25519 ( new Uint16Array ( privKey ) , new Uint16Array ( pubKey ) ) ) ) ;
2014-05-09 08:00:49 +02:00
}
} ) ;
}
2014-05-17 06:54:12 +02:00
var ECDHE = function ( pubKey , privKey ) { return testing _only . ECDHE ( pubKey , privKey ) ; }
2014-05-09 08:00:49 +02:00
2014-05-17 06:54:12 +02:00
testing _only . HKDF = function ( input , salt , info ) {
2014-05-09 08:00:49 +02:00
// Specific implementation of RFC 5869 that only returns exactly 64 bytes
2014-05-13 07:51:46 +02:00
return HmacSHA256 ( salt , input ) . then ( function ( PRK ) {
var infoBuffer = new ArrayBuffer ( info . byteLength + 1 + 32 ) ;
var infoArray = new Uint8Array ( infoBuffer ) ;
infoArray . set ( new Uint8Array ( info ) , 32 ) ;
infoArray [ infoArray . length - 1 ] = 0 ;
2014-05-09 08:00:49 +02:00
// TextSecure implements a slightly tweaked version of RFC 5869: the 0 and 1 should be 1 and 2 here
2014-05-13 07:51:46 +02:00
return HmacSHA256 ( PRK , infoBuffer . slice ( 32 ) ) . then ( function ( T1 ) {
infoArray . set ( new Uint8Array ( T1 ) ) ;
infoArray [ infoArray . length - 1 ] = 1 ;
return HmacSHA256 ( PRK , infoBuffer ) . then ( function ( T2 ) {
2014-05-09 08:00:49 +02:00
return [ T1 , T2 ] ;
} ) ;
} ) ;
} ) ;
}
var HKDF = function ( input , salt , info ) {
// HKDF for TextSecure has a bit of additional handling - salts always end up being 32 bytes
2014-05-13 07:51:46 +02:00
if ( salt == '' )
2014-05-09 08:00:49 +02:00
salt = new ArrayBuffer ( 32 ) ;
if ( salt . byteLength != 32 )
throw new Error ( "Got salt of incorrect length" ) ;
2014-05-13 07:51:46 +02:00
info = toArrayBuffer ( info ) ; // TODO: maybe convert calls?
2014-05-17 06:54:12 +02:00
return testing _only . HKDF ( input , salt , info ) ;
2014-05-09 08:00:49 +02:00
}
2014-05-13 07:51:46 +02:00
var calculateMACWithVersionByte = function ( data , key , version ) {
2014-05-09 08:00:49 +02:00
if ( version === undefined )
version = 1 ;
2014-05-13 07:51:46 +02:00
var prependedData = new Uint8Array ( data . byteLength + 1 ) ;
prependedData [ 0 ] = version ;
prependedData . set ( new Uint8Array ( data ) , 1 ) ;
2014-05-09 08:00:49 +02:00
2014-05-13 07:51:46 +02:00
return HmacSHA256 ( key , prependedData . buffer ) ;
2014-05-09 08:00:49 +02:00
}
2014-05-13 07:51:46 +02:00
var verifyMACWithVersionByte = function ( data , key , mac , version ) {
return calculateMACWithVersionByte ( data , key , version ) . then ( function ( calculated _mac ) {
var macString = getString ( mac ) ; //TODO: Move away from strings for comparison?
2014-05-09 08:00:49 +02:00
2014-05-13 07:51:46 +02:00
if ( getString ( calculated _mac ) . substring ( 0 , macString . length ) != macString )
throw new Error ( "Bad MAC" ) ;
} ) ;
2014-05-09 08:00:49 +02:00
}
2014-05-15 07:02:15 +02:00
var calculateMAC = function ( data , key ) {
return HmacSHA256 ( key , data ) ;
}
var verifyMAC = function ( data , key , mac ) {
return calculateMAC ( data , key ) . then ( function ( calculated _mac ) {
var macString = getString ( mac ) ; //TODO: Move away from strings for comparison?
if ( getString ( calculated _mac ) . substring ( 0 , macString . length ) != macString )
throw new Error ( "Bad MAC" ) ;
} ) ;
}
2014-05-09 08:00:49 +02:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Ratchet implementation * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
var calculateRatchet = function ( session , remoteKey , sending ) {
var ratchet = session . currentRatchet ;
2014-05-13 07:51:46 +02:00
return ECDHE ( remoteKey , toArrayBuffer ( ratchet . ephemeralKeyPair . privKey ) ) . then ( function ( sharedSecret ) {
return HKDF ( sharedSecret , toArrayBuffer ( ratchet . rootKey ) , "WhisperRatchet" ) . then ( function ( masterKey ) {
2014-05-09 08:00:49 +02:00
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 , encodedNumber , theirIdentityPubKey , theirEphemeralPubKey ) {
var ourIdentityPrivKey = crypto _storage . getIdentityPrivKey ( ) ;
2014-05-13 07:51:46 +02:00
var sharedSecret = new Uint8Array ( 32 * 3 ) ;
return ECDHE ( theirEphemeralPubKey , ourIdentityPrivKey ) . then ( function ( ecRes1 ) {
2014-05-09 08:00:49 +02:00
function finishInit ( ) {
return ECDHE ( theirEphemeralPubKey , ourEphemeralKey . privKey ) . then ( function ( ecRes ) {
2014-05-13 07:51:46 +02:00
sharedSecret . set ( new Uint8Array ( ecRes ) , 32 * 2 ) ;
2014-05-09 08:00:49 +02:00
2014-05-13 07:51:46 +02:00
return HKDF ( sharedSecret . buffer , '' , "WhisperText" ) . then ( function ( masterKey ) {
2014-05-09 08:00:49 +02:00
var session = { currentRatchet : { rootKey : masterKey [ 0 ] , lastRemoteEphemeralKey : theirEphemeralPubKey } ,
2014-06-10 02:26:34 +02:00
indexInfo : { remoteIdentityKey : theirIdentityPubKey , closed : - 1 , baseKey : theirEphemeralPubKey } ,
2014-05-09 08:00:49 +02:00
oldRatchetList : [ ]
} ;
// 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 createNewKeyPair ( false ) . then ( function ( ourSendingEphemeralKey ) {
session . currentRatchet . ephemeralKeyPair = ourSendingEphemeralKey ;
return calculateRatchet ( session , theirEphemeralPubKey , true ) . then ( function ( ) {
2014-05-14 11:10:05 +02:00
return session ;
2014-05-09 08:00:49 +02:00
} ) ;
} ) ;
} else {
session . currentRatchet . ephemeralKeyPair = ourEphemeralKey ;
2014-05-14 11:10:05 +02:00
return session ;
2014-05-09 08:00:49 +02:00
}
} ) ;
} ) ;
}
if ( isInitiator )
2014-05-13 07:51:46 +02:00
return ECDHE ( theirIdentityPubKey , ourEphemeralKey . privKey ) . then ( function ( ecRes2 ) {
sharedSecret . set ( new Uint8Array ( ecRes1 ) ) ;
sharedSecret . set ( new Uint8Array ( ecRes2 ) , 32 ) ;
2014-05-09 08:00:49 +02:00
} ) . then ( finishInit ) ;
else
2014-05-13 07:51:46 +02:00
return ECDHE ( theirIdentityPubKey , ourEphemeralKey . privKey ) . then ( function ( ecRes2 ) {
sharedSecret . set ( new Uint8Array ( ecRes1 ) , 32 ) ;
sharedSecret . set ( new Uint8Array ( ecRes2 ) )
2014-05-09 08:00:49 +02:00
} ) . then ( finishInit ) ;
} ) ;
}
2014-05-14 11:10:05 +02:00
var closeSession = function ( session ) {
// Clear any data which would allow session continuation:
// Lock down current receive ratchet
2014-06-10 02:14:52 +02:00
for ( key in session )
if ( key . chainKey !== undefined && key . chainKey . key !== undefined )
delete key . chainKey . key ;
2014-05-14 11:10:05 +02:00
// Delete current sending ratchet
2014-05-17 06:54:12 +02:00
delete session [ getString ( session . currentRatchet . ephemeralKeyPair . pubKey ) ] ;
2014-05-14 11:10:05 +02:00
// Delete current root key and our ephemeral key pair
delete session . currentRatchet [ 'rootKey' ] ;
delete session . currentRatchet [ 'ephemeralKeyPair' ] ;
session . indexInfo . closed = new Date ( ) . getTime ( ) ;
}
2014-05-09 08:00:49 +02:00
2014-06-03 23:44:30 +02:00
self . closeOpenSessionForDevice = function ( encodedNumber ) {
var session = crypto _storage . getOpenSession ( encodedNumber ) ;
if ( session === undefined )
return ;
closeSession ( session ) ;
crypto _storage . saveSession ( encodedNumber , session ) ;
}
2014-06-01 19:39:35 +02:00
var initSessionFromPreKeyWhisperMessage ;
var decryptWhisperMessage ;
var handlePreKeyWhisperMessage = function ( from , encodedMessage ) {
var preKeyProto = textsecure . protos . decodePreKeyWhisperMessageProtobuf ( encodedMessage ) ;
return initSessionFromPreKeyWhisperMessage ( from , preKeyProto ) . then ( function ( sessions ) {
return decryptWhisperMessage ( from , getString ( preKeyProto . message ) , sessions [ 0 ] , preKeyProto . registrationId ) . then ( function ( result ) {
if ( sessions [ 1 ] !== undefined )
crypto _storage . saveSession ( from , 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 ) {
2014-05-09 08:00:49 +02:00
var preKeyPair = crypto _storage . getAndRemovePreKeyPair ( message . preKeyId ) ;
2014-05-15 00:15:46 +02:00
2014-05-14 23:20:49 +02:00
var session = crypto _storage . getSessionOrIdentityKeyByBaseKey ( encodedNumber , toArrayBuffer ( message . baseKey ) ) ;
var open _session = crypto _storage . getOpenSession ( encodedNumber ) ;
2014-05-09 08:00:49 +02:00
if ( preKeyPair === undefined ) {
2014-05-14 11:10:05 +02:00
// Session may or may not be the correct 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 )
2014-05-14 20:27:08 +02:00
return Promise . resolve ( [ session , undefined ] ) ;
2014-05-09 08:00:49 +02:00
else
throw new Error ( "Missing preKey for PreKeyWhisperMessage" ) ;
2014-05-14 11:10:05 +02:00
}
if ( session !== undefined ) {
2014-05-14 23:20:49 +02:00
// We already had a session/known identity key:
2014-05-14 11:10:05 +02:00
if ( getString ( session . indexInfo . remoteIdentityKey ) == getString ( message . identityKey ) ) {
// If the identity key matches the previous one, close the previous one and use the new one
2014-05-14 23:20:49 +02:00
if ( open _session !== undefined )
closeSession ( open _session ) ; // To be returned and saved later
2014-05-14 11:10:05 +02:00
} else {
// ...otherwise create an error that the UI will pick up and ask the user if they want to re-negotiate
2014-06-01 19:39:35 +02:00
throw textsecure . createTryAgainError ( "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 ( ) ) ] ) ;
2014-05-14 11:10:05 +02:00
}
}
return initSession ( false , preKeyPair , encodedNumber , toArrayBuffer ( message . identityKey ) , toArrayBuffer ( message . baseKey ) )
. 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
2014-05-14 23:20:49 +02:00
return [ new _session , open _session ] ;
2014-05-14 11:10:05 +02:00
} ) ; ;
2014-05-09 08:00:49 +02:00
}
var fillMessageKeys = function ( chain , counter ) {
if ( chain . chainKey . counter + 1000 < counter ) //TODO: maybe 1000 is too low/high in some cases?
2014-05-14 09:33:24 +02:00
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 HmacSHA256 ( key , byteArray . buffer ) . then ( function ( mac ) {
byteArray [ 0 ] = 2 ;
return HmacSHA256 ( 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 ) ;
2014-05-09 08:00:49 +02:00
} ) ;
2014-05-14 09:33:24 +02:00
} ) ;
2014-05-09 08:00:49 +02:00
}
2014-05-14 09:02:47 +02:00
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 ) ) ;
2014-05-14 11:10:05 +02:00
if ( ! objectContainsKeys ( session [ ratchet ] . messageKeys ) || entry . added < new Date ( ) . getTime ( ) - MESSAGE _LOST _THRESHOLD _MS ) {
2014-05-14 09:02:47 +02:00
delete session [ ratchet ] ;
console . log ( "...deleted" ) ;
} else
newList [ newList . length ] = entry ;
}
session . oldRatchetList = newList ;
}
2014-05-09 08:00:49 +02:00
var maybeStepRatchet = function ( session , remoteKey , previousCounter ) {
if ( session [ getString ( remoteKey ) ] !== undefined )
2014-05-14 09:33:24 +02:00
return Promise . resolve ( ) ;
2014-05-09 08:00:49 +02:00
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 ;
2014-05-14 09:02:47 +02:00
delete session [ previousRatchet ] ;
2014-05-09 08:00:49 +02:00
} else
// TODO: This is just an idiosyncrasy upstream, which we match for testing
// it should be changed upstream to something more reasonable.
ratchet . previousCounter = 4294967295 ;
return createNewKeyPair ( false ) . 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 ( ) {
2014-06-10 02:14:52 +02:00
delete previousRatchet . chainKey . key ;
2014-05-09 08:00:49 +02:00
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
2014-06-01 19:39:35 +02:00
decryptWhisperMessage = function ( encodedNumber , messageBytes , session , registrationId ) {
2014-05-09 08:00:49 +02:00
if ( messageBytes [ 0 ] != String . fromCharCode ( ( 2 << 4 ) | 2 ) )
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 ) ;
2014-05-21 21:04:05 +02:00
var message = textsecure . protos . decodeWhisperMessageProtobuf ( messageProto ) ;
2014-05-14 11:10:05 +02:00
var remoteEphemeralKey = toArrayBuffer ( message . ephemeralKey ) ;
2014-05-09 08:00:49 +02:00
2014-05-14 11:10:05 +02:00
if ( session === undefined ) {
2014-05-14 23:20:49 +02:00
var session = crypto _storage . getSessionByRemoteEphemeralKey ( encodedNumber , remoteEphemeralKey ) ;
2014-05-14 11:10:05 +02:00
if ( session === undefined )
throw new Error ( "No session found to decrypt message from " + encodedNumber ) ;
}
return maybeStepRatchet ( session , remoteEphemeralKey , message . previousCounter ) . then ( function ( ) {
2014-05-09 08:00:49 +02:00
var chain = session [ getString ( message . ephemeralKey ) ] ;
return fillMessageKeys ( chain , message . counter ) . then ( function ( ) {
2014-05-13 07:51:46 +02:00
return HKDF ( toArrayBuffer ( chain . messageKeys [ message . counter ] ) , '' , "WhisperMessageKeys" ) . then ( function ( keys ) {
2014-05-09 08:00:49 +02:00
delete chain . messageKeys [ message . counter ] ;
2014-05-13 07:51:46 +02:00
return verifyMACWithVersionByte ( toArrayBuffer ( messageProto ) , keys [ 1 ] , mac , ( 2 << 4 ) | 2 ) . then ( function ( ) {
var counter = intToArrayBuffer ( message . counter ) ;
return window . crypto . subtle . decrypt ( { name : "AES-CTR" , counter : counter } , keys [ 0 ] , toArrayBuffer ( message . ciphertext ) )
2014-05-14 09:02:47 +02:00
. then ( function ( plaintext ) {
2014-05-09 09:43:23 +02:00
2014-05-14 09:02:47 +02:00
removeOldChains ( session ) ;
2014-05-13 07:51:46 +02:00
delete session [ 'pendingPreKey' ] ;
2014-05-09 08:00:49 +02:00
2014-05-21 21:04:05 +02:00
var finalMessage = textsecure . protos . decodePushMessageContentProtobuf ( getString ( plaintext ) ) ;
2014-05-14 11:10:05 +02:00
2014-06-03 23:44:30 +02:00
if ( ( finalMessage . flags & textsecure . protos . PushMessageContentProtobuf . Flags . END _SESSION )
== textsecure . protos . PushMessageContentProtobuf . Flags . END _SESSION )
2014-05-14 11:10:05 +02:00
closeSession ( session ) ;
2014-05-31 19:28:46 +02:00
crypto _storage . saveSession ( encodedNumber , session , registrationId ) ;
2014-05-14 11:10:05 +02:00
return finalMessage ;
2014-05-13 07:51:46 +02:00
} ) ;
2014-05-09 08:00:49 +02:00
} ) ;
} ) ;
} ) ;
} ) ;
}
/ * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Public crypto API * * *
* * * * * * * * * * * * * * * * * * * * * * * * * /
// Decrypts message into a raw string
2014-05-17 07:53:58 +02:00
self . decryptWebsocketMessage = function ( message ) {
2014-05-21 04:21:07 +02:00
var signaling _key = textsecure . storage . getEncrypted ( "signaling_key" ) ; //TODO: in crypto_storage
2014-05-13 21:15:45 +02:00
var aes _key = toArrayBuffer ( signaling _key . substring ( 0 , 32 ) ) ;
var mac _key = toArrayBuffer ( signaling _key . substring ( 32 , 32 + 20 ) ) ;
2014-05-09 08:00:49 +02:00
2014-05-13 21:15:45 +02:00
var decodedMessage = base64DecToArr ( getString ( message ) ) ;
if ( new Uint8Array ( decodedMessage ) [ 0 ] != 1 )
2014-05-09 08:00:49 +02:00
throw new Error ( "Got bad version number: " + decodedMessage [ 0 ] ) ;
2014-05-13 21:15:45 +02:00
var iv = decodedMessage . slice ( 1 , 1 + 16 ) ;
var ciphertext = decodedMessage . slice ( 1 + 16 , decodedMessage . byteLength - 10 ) ;
2014-05-15 06:26:37 +02:00
var ivAndCiphertext = decodedMessage . slice ( 1 , decodedMessage . byteLength - 10 ) ;
2014-05-13 21:15:45 +02:00
var mac = decodedMessage . slice ( decodedMessage . byteLength - 10 , decodedMessage . byteLength ) ;
2014-05-09 08:00:49 +02:00
2014-05-15 06:26:37 +02:00
return verifyMACWithVersionByte ( ivAndCiphertext , mac _key , mac ) . then ( function ( ) {
return window . crypto . subtle . decrypt ( { name : "AES-CBC" , iv : iv } , aes _key , ciphertext ) ;
} ) ;
} ;
2014-05-17 07:53:58 +02:00
self . decryptAttachment = function ( encryptedBin , keys ) {
2014-05-15 07:02:15 +02:00
var aes _key = keys . slice ( 0 , 32 ) ;
var mac _key = keys . slice ( 32 , 64 ) ;
2014-05-15 06:26:37 +02:00
2014-05-19 09:06:28 +02:00
var iv = encryptedBin . slice ( 0 , 16 ) ;
var ciphertext = encryptedBin . slice ( 16 , encryptedBin . byteLength - 32 ) ;
2014-05-15 06:26:37 +02:00
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 ( ) {
2014-05-09 09:43:23 +02:00
return window . crypto . subtle . decrypt ( { name : "AES-CBC" , iv : iv } , aes _key , ciphertext ) ;
2014-05-09 08:00:49 +02:00
} ) ;
} ;
2014-06-03 18:39:29 +02:00
self . encryptAttachment = function ( plaintext , keys , iv ) {
var aes _key = keys . slice ( 0 , 32 ) ;
var mac _key = keys . slice ( 32 , 64 ) ;
return window . crypto . subtle . encrypt ( { name : "AES-CBC" , iv : iv } , aes _key , plaintext ) . then ( function ( ciphertext ) {
var ivAndCiphertext = new Uint8Array ( 16 + ciphertext . byteLength ) ;
ivAndCiphertext . set ( iv ) ;
ivAndCiphertext . set ( ciphertext , 16 ) ;
return calculateMAC ( ivAndCiphertext . buffer , mac _key ) . then ( function ( mac ) {
var encryptedBin = new Uint8Array ( 16 + ciphertext . byteLength + 32 ) ;
encryptedBin . set ( ivAndCiphertext . buffer ) ;
encryptedBin . set ( mac , 16 + ciphertext . byteLength ) ;
return encryptedBin . buffer ;
} ) ;
} ) ;
} ;
2014-05-17 07:53:58 +02:00
self . handleIncomingPushMessageProto = function ( proto ) {
2014-05-09 08:00:49 +02:00
switch ( proto . type ) {
2014-06-03 18:39:29 +02:00
case textsecure . protos . IncomingPushMessageProtobuf . Type . PLAINTEXT :
2014-05-21 21:04:05 +02:00
return Promise . resolve ( textsecure . protos . decodePushMessageContentProtobuf ( getString ( proto . message ) ) ) ;
2014-06-03 18:39:29 +02:00
case textsecure . protos . IncomingPushMessageProtobuf . Type . CIPHERTEXT :
2014-05-26 01:48:41 +02:00
var from = proto . source + "." + ( proto . sourceDevice == null ? 0 : proto . sourceDevice ) ;
return decryptWhisperMessage ( from , getString ( proto . message ) ) ;
2014-06-03 18:39:29 +02:00
case textsecure . protos . IncomingPushMessageProtobuf . Type . PREKEY _BUNDLE :
2014-05-09 08:00:49 +02:00
if ( proto . message . readUint8 ( ) != ( 2 << 4 | 2 ) )
throw new Error ( "Bad version byte" ) ;
2014-05-26 01:48:41 +02:00
var from = proto . source + "." + ( proto . sourceDevice == null ? 0 : proto . sourceDevice ) ;
2014-06-01 19:39:35 +02:00
return handlePreKeyWhisperMessage ( from , getString ( proto . message ) ) ;
2014-06-03 18:39:29 +02:00
default :
return new Promise ( function ( resolve , reject ) { reject ( new Error ( "Unknown message type" ) ) ; } ) ;
2014-05-09 08:00:49 +02:00
}
}
2014-05-09 09:20:54 +02:00
// return Promise(encoded [PreKey]WhisperMessage)
2014-05-17 07:53:58 +02:00
self . encryptMessageFor = function ( deviceObject , pushMessageContent ) {
2014-05-14 23:20:49 +02:00
var session = crypto _storage . getOpenSession ( deviceObject . encodedNumber ) ;
2014-05-09 08:00:49 +02:00
var doEncryptPushMessageContent = function ( ) {
2014-05-21 21:04:05 +02:00
var msg = new textsecure . protos . WhisperMessageProtobuf ( ) ;
2014-05-09 08:00:49 +02:00
var plaintext = toArrayBuffer ( pushMessageContent . encode ( ) ) ;
msg . ephemeralKey = toArrayBuffer ( session . currentRatchet . ephemeralKeyPair . pubKey ) ;
var chain = session [ getString ( msg . ephemeralKey ) ] ;
return fillMessageKeys ( chain , chain . chainKey . counter + 1 ) . then ( function ( ) {
2014-05-13 07:51:46 +02:00
return HKDF ( toArrayBuffer ( chain . messageKeys [ chain . chainKey . counter ] ) , '' , "WhisperMessageKeys" ) . then ( function ( keys ) {
2014-05-09 08:00:49 +02:00
delete chain . messageKeys [ chain . chainKey . counter ] ;
msg . counter = chain . chainKey . counter ;
msg . previousCounter = session . currentRatchet . previousCounter ;
2014-05-09 09:43:23 +02:00
var counter = intToArrayBuffer ( chain . chainKey . counter ) ;
return window . crypto . subtle . encrypt ( { name : "AES-CTR" , counter : counter } , keys [ 0 ] , plaintext ) . then ( function ( ciphertext ) {
2014-05-09 08:00:49 +02:00
msg . ciphertext = ciphertext ;
2014-05-13 07:51:46 +02:00
var encodedMsg = toArrayBuffer ( msg . encode ( ) ) ;
2014-05-09 08:00:49 +02:00
return calculateMACWithVersionByte ( encodedMsg , keys [ 1 ] , ( 2 << 4 ) | 2 ) . then ( function ( mac ) {
2014-05-13 07:51:46 +02:00
var result = new Uint8Array ( encodedMsg . byteLength + 9 ) ;
result [ 0 ] = ( 2 << 4 ) | 2 ;
result . set ( new Uint8Array ( encodedMsg ) , 1 ) ;
result . set ( new Uint8Array ( mac , 0 , 8 ) , encodedMsg . byteLength + 1 ) ;
2014-05-09 08:00:49 +02:00
crypto _storage . saveSession ( deviceObject . encodedNumber , session ) ;
return result ;
} ) ;
} ) ;
} ) ;
} ) ;
}
2014-05-21 21:04:05 +02:00
var preKeyMsg = new textsecure . protos . PreKeyWhisperMessageProtobuf ( ) ;
2014-05-09 08:00:49 +02:00
preKeyMsg . identityKey = toArrayBuffer ( crypto _storage . getStoredPubKey ( "identityKey" ) ) ;
preKeyMsg . preKeyId = deviceObject . preKeyId ;
2014-05-21 04:21:07 +02:00
preKeyMsg . registrationId = textsecure . storage . getUnencrypted ( "registrationId" ) ;
2014-05-09 08:00:49 +02:00
if ( session === undefined ) {
2014-05-09 09:20:54 +02:00
return createNewKeyPair ( false ) . then ( function ( baseKey ) {
2014-05-09 08:00:49 +02:00
preKeyMsg . baseKey = toArrayBuffer ( baseKey . pubKey ) ;
2014-05-13 10:40:29 +02:00
return initSession ( true , baseKey , deviceObject . encodedNumber ,
toArrayBuffer ( deviceObject . identityKey ) , toArrayBuffer ( deviceObject . publicKey ) )
2014-05-14 11:10:05 +02:00
. then ( function ( new _session ) {
session = new _session ;
2014-05-09 08:00:49 +02:00
session . pendingPreKey = baseKey . pubKey ;
2014-05-09 09:20:54 +02:00
return doEncryptPushMessageContent ( ) . then ( function ( message ) {
2014-05-13 07:51:46 +02:00
preKeyMsg . message = message ;
2014-05-09 08:00:49 +02:00
var result = String . fromCharCode ( ( 2 << 4 ) | 2 ) + getString ( preKeyMsg . encode ( ) ) ;
2014-05-09 09:20:54 +02:00
return { type : 3 , body : result } ;
2014-05-09 08:00:49 +02:00
} ) ;
} ) ;
} ) ;
} else
2014-05-09 09:20:54 +02:00
return doEncryptPushMessageContent ( ) . then ( function ( message ) {
2014-05-09 08:00:49 +02:00
if ( session . pendingPreKey !== undefined ) {
preKeyMsg . baseKey = toArrayBuffer ( session . pendingPreKey ) ;
2014-05-13 07:51:46 +02:00
preKeyMsg . message = message ;
2014-05-09 08:00:49 +02:00
var result = String . fromCharCode ( ( 2 << 4 ) | 2 ) + getString ( preKeyMsg . encode ( ) ) ;
2014-05-09 09:20:54 +02:00
return { type : 3 , body : result } ;
2014-05-09 08:00:49 +02:00
} else
2014-05-09 09:20:54 +02:00
return { type : 1 , body : getString ( message ) } ;
2014-05-09 08:00:49 +02:00
} ) ;
}
var GENERATE _KEYS _KEYS _GENERATED = 100 ;
2014-05-17 07:53:58 +02:00
self . generateKeys = function ( ) {
2014-05-09 08:00:49 +02:00
var identityKey = crypto _storage . getStoredPubKey ( "identityKey" ) ;
var identityKeyCalculated = function ( pubKey ) {
identityKey = pubKey ;
2014-05-21 04:21:07 +02:00
var firstKeyId = textsecure . storage . getEncrypted ( "maxPreKeyId" , - 1 ) + 1 ;
textsecure . storage . putEncrypted ( "maxPreKeyId" , firstKeyId + GENERATE _KEYS _KEYS _GENERATED ) ;
2014-05-09 08:00:49 +02:00
if ( firstKeyId > 16777000 )
return new Promise ( function ( ) { throw new Error ( "You crazy motherfucker" ) } ) ;
var keys = { } ;
keys . keys = [ ] ;
2014-05-13 10:40:29 +02:00
var generateKey = function ( keyId ) {
return crypto _storage . getNewPubKeySTORINGPrivKey ( "preKey" + keyId , false ) . then ( function ( pubKey ) {
keys . keys [ keyId ] = { keyId : keyId , publicKey : pubKey , identityKey : identityKey } ;
} ) ;
} ;
var promises = [ ] ;
for ( var i = firstKeyId ; i < firstKeyId + GENERATE _KEYS _KEYS _GENERATED ; i ++ )
promises [ i ] = generateKey ( i ) ;
return Promise . all ( promises ) . then ( function ( ) {
// 0xFFFFFF == 16777215
keys . lastResortKey = { keyId : 16777215 , publicKey : crypto _storage . getStoredPubKey ( "preKey16777215" ) , identityKey : identityKey } ; //TODO: Rotate lastResortKey
if ( keys . lastResortKey . publicKey === undefined ) {
return crypto _storage . getNewPubKeySTORINGPrivKey ( "preKey16777215" , false ) . then ( function ( pubKey ) {
keys . lastResortKey . publicKey = pubKey ;
return keys ;
2014-05-09 08:00:49 +02:00
} ) ;
2014-05-13 10:40:29 +02:00
} else
return keys ;
2014-05-09 08:00:49 +02:00
} ) ;
}
if ( identityKey === undefined )
return crypto _storage . getNewPubKeySTORINGPrivKey ( "identityKey" , true ) . then ( function ( pubKey ) { return identityKeyCalculated ( pubKey ) ; } ) ;
else
return identityKeyCalculated ( identityKey ) ;
}
2014-05-17 06:54:12 +02:00
2014-05-17 07:53:58 +02:00
self . testing _only = testing _only ;
return self ;
2014-05-17 06:54:12 +02:00
} ( ) ;