2014-05-14 18:58:12 +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/>.
* /
2014-05-17 05:53:58 +00:00
window . textsecure = window . textsecure || { } ;
2014-05-09 06:00:49 +00:00
2014-05-28 01:53:43 +00:00
window . textsecure . crypto = function ( ) {
2014-05-17 05:53:58 +00:00
var self = { } ;
2014-05-17 04:54:12 +00:00
// functions exposed for replacement and direct calling in test code
var testing _only = { } ;
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Random constants / utils * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
2014-05-14 09:10:05 +00: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 04:54:12 +00:00
var getRandomBytes = function ( size ) {
2014-05-09 06:00:49 +00:00
//TODO: Better random (https://www.grc.com/r&d/js.htm?)
try {
var buffer = new ArrayBuffer ( size ) ;
var array = new Uint8Array ( buffer ) ;
window . crypto . getRandomValues ( array ) ;
return buffer ;
} catch ( err ) {
//TODO: ummm...wat?
throw err ;
}
}
2014-05-17 05:53:58 +00:00
self . getRandomBytes = getRandomBytes ;
2014-05-17 04:54:12 +00: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 06:00:49 +00:00
2014-05-21 02:21:07 +00:00
function objectContainsKeys ( object ) {
var count = 0 ;
for ( key in object ) {
count ++ ;
break ;
}
return count != 0 ;
}
2014-05-09 07:43:23 +00:00
function HmacSHA256 ( key , input ) {
return window . crypto . subtle . sign ( { name : "HMAC" , hash : "SHA-256" } , key , input ) ;
}
2014-05-17 04:54:12 +00:00
testing _only . privToPub = function ( privKey , isIdentity ) {
2014-05-09 06:00:49 +00: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 05:51:46 +00:00
pubWithPrefix . set ( origPub , 1 ) ;
2014-05-09 06:00:49 +00:00
pubWithPrefix [ 0 ] = 5 ;
return pub ;
}
2014-05-17 05:53:58 +00:00
if ( textsecure . nacl . USE _NACL ) {
2014-05-17 05:55:32 +00:00
return textsecure . nacl . postNaclMessage ( { command : "bytesToPriv" , priv : privKey } ) . then ( function ( message ) {
2014-05-09 07:20:54 +00:00
var priv = message . res ;
if ( ! isIdentity )
new Uint8Array ( priv ) [ 0 ] |= 0x01 ;
2014-05-17 05:55:32 +00:00
return textsecure . nacl . postNaclMessage ( { command : "privToPub" , priv : priv } ) . then ( function ( message ) {
2014-05-09 07:20:54 +00:00
return { pubKey : prependVersion ( message . res ) , privKey : priv } ;
2014-05-09 06:00:49 +00: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 19:58:53 +00:00
return Promise . resolve ( { pubKey : prependVersion ( toArrayBuffer ( curve25519 ( priv ) ) ) , privKey : privKey } ) ;
2014-05-09 06:00:49 +00:00
}
}
2014-05-17 04:54:12 +00:00
var privToPub = function ( privKey , isIdentity ) { return testing _only . privToPub ( privKey , isIdentity ) ; }
2014-05-09 06:00:49 +00:00
2014-05-17 04:54:12 +00:00
testing _only . createNewKeyPair = function ( isIdentity ) {
return privToPub ( getRandomBytes ( 32 ) , isIdentity ) ;
2014-05-09 06:00:49 +00:00
}
2014-05-17 04:54:12 +00:00
var createNewKeyPair = function ( isIdentity ) { return testing _only . createNewKeyPair ( isIdentity ) ; }
2014-05-09 06:00:49 +00:00
2014-05-17 04:54:12 +00:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Key / session storage * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * /
2014-05-09 06:00:49 +00:00
var crypto _storage = { } ;
crypto _storage . getNewPubKeySTORINGPrivKey = function ( keyName , isIdentity ) {
return createNewKeyPair ( isIdentity ) . then ( function ( keyPair ) {
2014-05-21 02:21:07 +00:00
textsecure . storage . putEncrypted ( "25519Key" + keyName , keyPair ) ;
2014-05-09 06:00:49 +00:00
return keyPair . pubKey ;
} ) ;
}
crypto _storage . getStoredPubKey = function ( keyName ) {
2014-05-21 02:21:07 +00:00
return toArrayBuffer ( textsecure . storage . getEncrypted ( "25519Key" + keyName , { pubKey : undefined } ) . pubKey ) ;
2014-05-09 06:00:49 +00:00
}
crypto _storage . getStoredKeyPair = function ( keyName ) {
2014-05-21 02:21:07 +00:00
var res = textsecure . storage . getEncrypted ( "25519Key" + keyName ) ;
2014-05-09 06:00:49 +00: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 02:21:07 +00:00
textsecure . storage . removeEncrypted ( "25519Key" + keyName ) ;
2014-05-09 06:00:49 +00:00
return keyPair ;
}
crypto _storage . getAndRemovePreKeyPair = function ( keyId ) {
return this . getAndRemoveStoredKeyPair ( "preKey" + keyId ) ;
}
crypto _storage . getIdentityPrivKey = function ( ) {
return this . getStoredKeyPair ( "identityKey" ) . privKey ;
}
crypto _storage . saveSession = function ( encodedNumber , session ) {
2014-05-22 05:14:46 +00:00
var sessions = textsecure . storage . getEncrypted ( "session" + encodedNumber ) ;
2014-05-14 09:10:05 +00:00
if ( sessions === undefined )
sessions = { } ;
var doDeleteSession = false ;
if ( session . indexInfo . closed == - 1 )
sessions . identityKey = session . indexInfo . remoteIdentityKey ;
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 06:00:49 +00:00
2014-05-14 09:10:05 +00:00
if ( doDeleteSession )
delete sessions [ getString ( session . indexInfo . baseKey ) ] ;
else
sessions [ getString ( session . indexInfo . baseKey ) ] = session ;
2014-05-22 05:14:46 +00:00
textsecure . storage . putEncrypted ( "session" + encodedNumber , sessions ) ;
2014-05-09 06:00:49 +00:00
}
2014-05-14 21:20:49 +00:00
crypto _storage . getOpenSession = function ( encodedNumber ) {
2014-05-22 05:14:46 +00:00
var sessions = textsecure . storage . getEncrypted ( "session" + encodedNumber ) ;
2014-05-14 09:10:05 +00:00
if ( sessions === undefined )
return undefined ;
2014-05-14 21:20:49 +00:00
for ( key in sessions ) {
if ( key == "identityKey" )
continue ;
2014-05-14 09:10:05 +00:00
2014-05-14 21:20:49 +00:00
if ( sessions [ key ] . indexInfo . closed == - 1 )
return sessions [ key ] ;
}
return undefined ;
}
crypto _storage . getSessionByRemoteEphemeralKey = function ( encodedNumber , remoteEphemeralKey ) {
2014-05-22 05:14:46 +00:00
var sessions = textsecure . storage . getEncrypted ( "session" + encodedNumber ) ;
2014-05-14 21:20:49 +00:00
if ( sessions === undefined )
return undefined ;
var searchKey = getString ( remoteEphemeralKey ) ;
2014-05-14 09:10:05 +00:00
var openSession = undefined ;
for ( key in sessions ) {
if ( key == "identityKey" )
continue ;
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 21:20:49 +00:00
return undefined ;
}
crypto _storage . getSessionOrIdentityKeyByBaseKey = function ( encodedNumber , baseKey ) {
2014-05-22 05:14:46 +00:00
var sessions = textsecure . storage . getEncrypted ( "session" + encodedNumber ) ;
2014-05-14 21:20:49 +00:00
if ( sessions === undefined )
return undefined ;
var preferredSession = sessions [ getString ( baseKey ) ] ;
if ( preferredSession !== undefined )
return preferredSession ;
if ( sessions . identityKey !== undefined )
2014-05-14 09:10:05 +00:00
return { indexInfo : { remoteIdentityKey : sessions . identityKey } } ;
2014-05-14 21:20:49 +00:00
throw new Error ( "Datastore inconsistency: session was stored without identity key" ) ;
2014-05-14 09:10:05 +00:00
}
2014-05-09 06:00:49 +00:00
2014-05-27 23:14:16 +00:00
// Used when device keys change - we assume key compromise so refuse all new messages
self . forceRemoveAllSessions = function ( encodedNumber ) {
textsecure . storage . removeEncrypted ( "session" + encodedNumber ) ;
}
2014-05-14 21:20:49 +00:00
2014-05-09 06:00:49 +00:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Internal Crypto stuff * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
2014-05-17 04:54:12 +00:00
testing _only . ECDHE = function ( pubKey , privKey ) {
2014-05-13 05:51:46 +00:00
if ( privKey === undefined || privKey . byteLength != 32 )
2014-05-09 06:00:49 +00:00
throw new Error ( "Invalid private key" ) ;
2014-05-13 08:40:29 +00:00
if ( pubKey === undefined || ( ( pubKey . byteLength != 33 || new Uint8Array ( pubKey ) [ 0 ] != 5 ) && pubKey . byteLength != 32 ) )
2014-05-13 05:51:46 +00:00
throw new Error ( "Invalid public key" ) ;
2014-05-13 08:40:29 +00: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 06:00:49 +00:00
return new Promise ( function ( resolve ) {
2014-05-17 05:53:58 +00:00
if ( textsecure . nacl . USE _NACL ) {
2014-05-17 05:55:32 +00:00
textsecure . nacl . postNaclMessage ( { command : "ECDHE" , priv : privKey , pub : pubKey } ) . then ( function ( message ) {
2014-05-09 06:00:49 +00:00
resolve ( message . res ) ;
} ) ;
} else {
2014-05-13 08:40:29 +00:00
resolve ( toArrayBuffer ( curve25519 ( new Uint16Array ( privKey ) , new Uint16Array ( pubKey ) ) ) ) ;
2014-05-09 06:00:49 +00:00
}
} ) ;
}
2014-05-17 04:54:12 +00:00
var ECDHE = function ( pubKey , privKey ) { return testing _only . ECDHE ( pubKey , privKey ) ; }
2014-05-09 06:00:49 +00:00
2014-05-17 04:54:12 +00:00
testing _only . HKDF = function ( input , salt , info ) {
2014-05-09 06:00:49 +00:00
// Specific implementation of RFC 5869 that only returns exactly 64 bytes
2014-05-13 05:51:46 +00: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 06:00:49 +00:00
// TextSecure implements a slightly tweaked version of RFC 5869: the 0 and 1 should be 1 and 2 here
2014-05-13 05:51:46 +00: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 06:00:49 +00: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 05:51:46 +00:00
if ( salt == '' )
2014-05-09 06:00:49 +00:00
salt = new ArrayBuffer ( 32 ) ;
if ( salt . byteLength != 32 )
throw new Error ( "Got salt of incorrect length" ) ;
2014-05-13 05:51:46 +00:00
info = toArrayBuffer ( info ) ; // TODO: maybe convert calls?
2014-05-17 04:54:12 +00:00
return testing _only . HKDF ( input , salt , info ) ;
2014-05-09 06:00:49 +00:00
}
2014-05-13 05:51:46 +00:00
var calculateMACWithVersionByte = function ( data , key , version ) {
2014-05-09 06:00:49 +00:00
if ( version === undefined )
version = 1 ;
2014-05-13 05:51:46 +00:00
var prependedData = new Uint8Array ( data . byteLength + 1 ) ;
prependedData [ 0 ] = version ;
prependedData . set ( new Uint8Array ( data ) , 1 ) ;
2014-05-09 06:00:49 +00:00
2014-05-13 05:51:46 +00:00
return HmacSHA256 ( key , prependedData . buffer ) ;
2014-05-09 06:00:49 +00:00
}
2014-05-13 05:51:46 +00: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 06:00:49 +00:00
2014-05-13 05:51:46 +00:00
if ( getString ( calculated _mac ) . substring ( 0 , macString . length ) != macString )
throw new Error ( "Bad MAC" ) ;
} ) ;
2014-05-09 06:00:49 +00:00
}
2014-05-15 05:02:15 +00: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 06:00:49 +00:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Ratchet implementation * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
var calculateRatchet = function ( session , remoteKey , sending ) {
var ratchet = session . currentRatchet ;
2014-05-13 05:51:46 +00:00
return ECDHE ( remoteKey , toArrayBuffer ( ratchet . ephemeralKeyPair . privKey ) ) . then ( function ( sharedSecret ) {
return HKDF ( sharedSecret , toArrayBuffer ( ratchet . rootKey ) , "WhisperRatchet" ) . then ( function ( masterKey ) {
2014-05-09 06:00:49 +00: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 05:51:46 +00:00
var sharedSecret = new Uint8Array ( 32 * 3 ) ;
return ECDHE ( theirEphemeralPubKey , ourIdentityPrivKey ) . then ( function ( ecRes1 ) {
2014-05-09 06:00:49 +00:00
function finishInit ( ) {
return ECDHE ( theirEphemeralPubKey , ourEphemeralKey . privKey ) . then ( function ( ecRes ) {
2014-05-13 05:51:46 +00:00
sharedSecret . set ( new Uint8Array ( ecRes ) , 32 * 2 ) ;
2014-05-09 06:00:49 +00:00
2014-05-13 05:51:46 +00:00
return HKDF ( sharedSecret . buffer , '' , "WhisperText" ) . then ( function ( masterKey ) {
2014-05-09 06:00:49 +00:00
var session = { currentRatchet : { rootKey : masterKey [ 0 ] , lastRemoteEphemeralKey : theirEphemeralPubKey } ,
2014-05-14 09:10:05 +00:00
indexInfo : { remoteIdentityKey : theirIdentityPubKey , closed : - 1 } ,
2014-05-09 06:00:49 +00: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 09:10:05 +00:00
return session ;
2014-05-09 06:00:49 +00:00
} ) ;
} ) ;
} else {
session . currentRatchet . ephemeralKeyPair = ourEphemeralKey ;
2014-05-14 09:10:05 +00:00
return session ;
2014-05-09 06:00:49 +00:00
}
} ) ;
} ) ;
}
if ( isInitiator )
2014-05-13 05:51:46 +00:00
return ECDHE ( theirIdentityPubKey , ourEphemeralKey . privKey ) . then ( function ( ecRes2 ) {
sharedSecret . set ( new Uint8Array ( ecRes1 ) ) ;
sharedSecret . set ( new Uint8Array ( ecRes2 ) , 32 ) ;
2014-05-09 06:00:49 +00:00
} ) . then ( finishInit ) ;
else
2014-05-13 05:51:46 +00:00
return ECDHE ( theirIdentityPubKey , ourEphemeralKey . privKey ) . then ( function ( ecRes2 ) {
sharedSecret . set ( new Uint8Array ( ecRes1 ) , 32 ) ;
sharedSecret . set ( new Uint8Array ( ecRes2 ) )
2014-05-09 06:00:49 +00:00
} ) . then ( finishInit ) ;
} ) ;
}
2014-05-14 09:10:05 +00:00
var closeSession = function ( session ) {
// Clear any data which would allow session continuation:
// Lock down current receive ratchet
// TODO: Some kind of delete chainKey['key']
// Delete current sending ratchet
2014-05-17 04:54:12 +00:00
delete session [ getString ( session . currentRatchet . ephemeralKeyPair . pubKey ) ] ;
2014-05-14 09:10:05 +00: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 06:00:49 +00:00
2014-05-14 09:10:05 +00:00
var initSessionFromPreKeyWhisperMessage = function ( encodedNumber , message ) {
2014-05-09 06:00:49 +00:00
var preKeyPair = crypto _storage . getAndRemovePreKeyPair ( message . preKeyId ) ;
2014-05-14 22:15:46 +00:00
2014-05-14 21:20:49 +00:00
var session = crypto _storage . getSessionOrIdentityKeyByBaseKey ( encodedNumber , toArrayBuffer ( message . baseKey ) ) ;
var open _session = crypto _storage . getOpenSession ( encodedNumber ) ;
2014-05-09 06:00:49 +00:00
if ( preKeyPair === undefined ) {
2014-05-14 09:10:05 +00: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 18:27:08 +00:00
return Promise . resolve ( [ session , undefined ] ) ;
2014-05-09 06:00:49 +00:00
else
throw new Error ( "Missing preKey for PreKeyWhisperMessage" ) ;
2014-05-14 09:10:05 +00:00
}
if ( session !== undefined ) {
2014-05-14 21:20:49 +00:00
// We already had a session/known identity key:
2014-05-14 09:10:05 +00: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 21:20:49 +00:00
if ( open _session !== undefined )
closeSession ( open _session ) ; // To be returned and saved later
2014-05-14 09:10:05 +00:00
} else {
// ...otherwise create an error that the UI will pick up and ask the user if they want to re-negotiate
// TODO: Save the message for possible later renegotiation
2014-05-27 19:29:44 +00:00
textsecure . throwHumanError ( "Received message with unknown identity key" , "WarnTryAgainError" , "The identity of the sender has changed. This may be malicious, or the sender may have simply reinstalled TextSecure." ) ;
2014-05-14 09:10:05 +00: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
new _session . indexInfo . baseKey = message . baseKey ;
2014-05-14 21:20:49 +00:00
return [ new _session , open _session ] ;
2014-05-14 09:10:05 +00:00
} ) ; ;
2014-05-09 06:00:49 +00: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 07:33:24 +00: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 06:00:49 +00:00
} ) ;
2014-05-14 07:33:24 +00:00
} ) ;
2014-05-09 06:00:49 +00:00
}
2014-05-14 07:02:47 +00: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 09:10:05 +00:00
if ( ! objectContainsKeys ( session [ ratchet ] . messageKeys ) || entry . added < new Date ( ) . getTime ( ) - MESSAGE _LOST _THRESHOLD _MS ) {
2014-05-14 07:02:47 +00:00
delete session [ ratchet ] ;
console . log ( "...deleted" ) ;
} else
newList [ newList . length ] = entry ;
}
session . oldRatchetList = newList ;
}
2014-05-09 06:00:49 +00:00
var maybeStepRatchet = function ( session , remoteKey , previousCounter ) {
if ( session [ getString ( remoteKey ) ] !== undefined )
2014-05-14 07:33:24 +00:00
return Promise . resolve ( ) ;
2014-05-09 06:00:49 +00: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 07:02:47 +00:00
delete session [ previousRatchet ] ;
2014-05-09 06:00:49 +00: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-05-14 07:33:24 +00:00
delete previousRatchet . chainKey [ 'key' ] ;
2014-05-09 06:00:49 +00: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-05-14 09:10:05 +00:00
var decryptWhisperMessage = function ( encodedNumber , messageBytes , session ) {
2014-05-09 06:00:49 +00: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 19:04:05 +00:00
var message = textsecure . protos . decodeWhisperMessageProtobuf ( messageProto ) ;
2014-05-14 09:10:05 +00:00
var remoteEphemeralKey = toArrayBuffer ( message . ephemeralKey ) ;
2014-05-09 06:00:49 +00:00
2014-05-14 09:10:05 +00:00
if ( session === undefined ) {
2014-05-14 21:20:49 +00:00
var session = crypto _storage . getSessionByRemoteEphemeralKey ( encodedNumber , remoteEphemeralKey ) ;
2014-05-14 09:10:05 +00: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 06:00:49 +00:00
var chain = session [ getString ( message . ephemeralKey ) ] ;
return fillMessageKeys ( chain , message . counter ) . then ( function ( ) {
2014-05-13 05:51:46 +00:00
return HKDF ( toArrayBuffer ( chain . messageKeys [ message . counter ] ) , '' , "WhisperMessageKeys" ) . then ( function ( keys ) {
2014-05-09 06:00:49 +00:00
delete chain . messageKeys [ message . counter ] ;
2014-05-13 05:51:46 +00: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 07:02:47 +00:00
. then ( function ( plaintext ) {
2014-05-09 07:43:23 +00:00
2014-05-14 07:02:47 +00:00
removeOldChains ( session ) ;
2014-05-13 05:51:46 +00:00
delete session [ 'pendingPreKey' ] ;
2014-05-09 06:00:49 +00:00
2014-05-21 19:04:05 +00:00
var finalMessage = textsecure . protos . decodePushMessageContentProtobuf ( getString ( plaintext ) ) ;
2014-05-14 09:10:05 +00:00
if ( ( finalMessage . flags & 1 ) == 1 ) // END_SESSION
closeSession ( session ) ;
2014-05-13 05:51:46 +00:00
crypto _storage . saveSession ( encodedNumber , session ) ;
2014-05-14 09:10:05 +00:00
return finalMessage ;
2014-05-13 05:51:46 +00:00
} ) ;
2014-05-09 06:00:49 +00:00
} ) ;
} ) ;
} ) ;
} ) ;
}
/ * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Public crypto API * * *
* * * * * * * * * * * * * * * * * * * * * * * * * /
// Decrypts message into a raw string
2014-05-17 05:53:58 +00:00
self . decryptWebsocketMessage = function ( message ) {
2014-05-21 02:21:07 +00:00
var signaling _key = textsecure . storage . getEncrypted ( "signaling_key" ) ; //TODO: in crypto_storage
2014-05-13 19:15:45 +00:00
var aes _key = toArrayBuffer ( signaling _key . substring ( 0 , 32 ) ) ;
var mac _key = toArrayBuffer ( signaling _key . substring ( 32 , 32 + 20 ) ) ;
2014-05-09 06:00:49 +00:00
2014-05-13 19:15:45 +00:00
var decodedMessage = base64DecToArr ( getString ( message ) ) ;
if ( new Uint8Array ( decodedMessage ) [ 0 ] != 1 )
2014-05-09 06:00:49 +00:00
throw new Error ( "Got bad version number: " + decodedMessage [ 0 ] ) ;
2014-05-13 19:15:45 +00:00
var iv = decodedMessage . slice ( 1 , 1 + 16 ) ;
var ciphertext = decodedMessage . slice ( 1 + 16 , decodedMessage . byteLength - 10 ) ;
2014-05-15 04:26:37 +00:00
var ivAndCiphertext = decodedMessage . slice ( 1 , decodedMessage . byteLength - 10 ) ;
2014-05-13 19:15:45 +00:00
var mac = decodedMessage . slice ( decodedMessage . byteLength - 10 , decodedMessage . byteLength ) ;
2014-05-09 06:00:49 +00:00
2014-05-15 04:26:37 +00: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 05:53:58 +00:00
self . decryptAttachment = function ( encryptedBin , keys ) {
2014-05-15 05:02:15 +00:00
var aes _key = keys . slice ( 0 , 32 ) ;
var mac _key = keys . slice ( 32 , 64 ) ;
2014-05-15 04:26:37 +00:00
2014-05-19 07:06:28 +00:00
var iv = encryptedBin . slice ( 0 , 16 ) ;
var ciphertext = encryptedBin . slice ( 16 , encryptedBin . byteLength - 32 ) ;
2014-05-15 04:26:37 +00: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 07:43:23 +00:00
return window . crypto . subtle . decrypt ( { name : "AES-CBC" , iv : iv } , aes _key , ciphertext ) ;
2014-05-09 06:00:49 +00:00
} ) ;
} ;
2014-05-17 05:53:58 +00:00
self . handleIncomingPushMessageProto = function ( proto ) {
2014-05-09 06:00:49 +00:00
switch ( proto . type ) {
case 0 : //TYPE_MESSAGE_PLAINTEXT
2014-05-21 19:04:05 +00:00
return Promise . resolve ( textsecure . protos . decodePushMessageContentProtobuf ( getString ( proto . message ) ) ) ;
2014-05-09 06:00:49 +00:00
case 1 : //TYPE_MESSAGE_CIPHERTEXT
2014-05-25 23:48:41 +00:00
var from = proto . source + "." + ( proto . sourceDevice == null ? 0 : proto . sourceDevice ) ;
return decryptWhisperMessage ( from , getString ( proto . message ) ) ;
2014-05-09 06:00:49 +00:00
case 3 : //TYPE_MESSAGE_PREKEY_BUNDLE
if ( proto . message . readUint8 ( ) != ( 2 << 4 | 2 ) )
throw new Error ( "Bad version byte" ) ;
2014-05-25 23:48:41 +00:00
var from = proto . source + "." + ( proto . sourceDevice == null ? 0 : proto . sourceDevice ) ;
2014-05-21 19:04:05 +00:00
var preKeyProto = textsecure . protos . decodePreKeyWhisperMessageProtobuf ( getString ( proto . message ) ) ;
2014-05-25 23:48:41 +00:00
return initSessionFromPreKeyWhisperMessage ( from , preKeyProto ) . then ( function ( sessions ) {
return decryptWhisperMessage ( from , getString ( preKeyProto . message ) , sessions [ 0 ] ) . then ( function ( result ) {
2014-05-14 18:27:08 +00:00
if ( sessions [ 1 ] !== undefined )
crypto _storage . saveSession ( proto . source , sessions [ 1 ] ) ;
2014-05-25 22:16:09 +00:00
return result ;
2014-05-09 06:00:49 +00:00
} ) ;
} ) ;
}
}
2014-05-09 07:20:54 +00:00
// return Promise(encoded [PreKey]WhisperMessage)
2014-05-17 05:53:58 +00:00
self . encryptMessageFor = function ( deviceObject , pushMessageContent ) {
2014-05-14 21:20:49 +00:00
var session = crypto _storage . getOpenSession ( deviceObject . encodedNumber ) ;
2014-05-09 06:00:49 +00:00
var doEncryptPushMessageContent = function ( ) {
2014-05-21 19:04:05 +00:00
var msg = new textsecure . protos . WhisperMessageProtobuf ( ) ;
2014-05-09 06:00:49 +00: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 05:51:46 +00:00
return HKDF ( toArrayBuffer ( chain . messageKeys [ chain . chainKey . counter ] ) , '' , "WhisperMessageKeys" ) . then ( function ( keys ) {
2014-05-09 06:00:49 +00:00
delete chain . messageKeys [ chain . chainKey . counter ] ;
msg . counter = chain . chainKey . counter ;
msg . previousCounter = session . currentRatchet . previousCounter ;
2014-05-09 07:43:23 +00: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 06:00:49 +00:00
msg . ciphertext = ciphertext ;
2014-05-13 05:51:46 +00:00
var encodedMsg = toArrayBuffer ( msg . encode ( ) ) ;
2014-05-09 06:00:49 +00:00
return calculateMACWithVersionByte ( encodedMsg , keys [ 1 ] , ( 2 << 4 ) | 2 ) . then ( function ( mac ) {
2014-05-13 05:51:46 +00: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 06:00:49 +00:00
crypto _storage . saveSession ( deviceObject . encodedNumber , session ) ;
return result ;
} ) ;
} ) ;
} ) ;
} ) ;
}
2014-05-21 19:04:05 +00:00
var preKeyMsg = new textsecure . protos . PreKeyWhisperMessageProtobuf ( ) ;
2014-05-09 06:00:49 +00:00
preKeyMsg . identityKey = toArrayBuffer ( crypto _storage . getStoredPubKey ( "identityKey" ) ) ;
preKeyMsg . preKeyId = deviceObject . preKeyId ;
2014-05-21 02:21:07 +00:00
preKeyMsg . registrationId = textsecure . storage . getUnencrypted ( "registrationId" ) ;
2014-05-09 06:00:49 +00:00
if ( session === undefined ) {
2014-05-09 07:20:54 +00:00
return createNewKeyPair ( false ) . then ( function ( baseKey ) {
2014-05-09 06:00:49 +00:00
preKeyMsg . baseKey = toArrayBuffer ( baseKey . pubKey ) ;
2014-05-13 08:40:29 +00:00
return initSession ( true , baseKey , deviceObject . encodedNumber ,
toArrayBuffer ( deviceObject . identityKey ) , toArrayBuffer ( deviceObject . publicKey ) )
2014-05-14 09:10:05 +00:00
. then ( function ( new _session ) {
session = new _session ;
2014-05-09 06:00:49 +00:00
session . pendingPreKey = baseKey . pubKey ;
2014-05-09 07:20:54 +00:00
return doEncryptPushMessageContent ( ) . then ( function ( message ) {
2014-05-13 05:51:46 +00:00
preKeyMsg . message = message ;
2014-05-09 06:00:49 +00:00
var result = String . fromCharCode ( ( 2 << 4 ) | 2 ) + getString ( preKeyMsg . encode ( ) ) ;
2014-05-09 07:20:54 +00:00
return { type : 3 , body : result } ;
2014-05-09 06:00:49 +00:00
} ) ;
} ) ;
} ) ;
} else
2014-05-09 07:20:54 +00:00
return doEncryptPushMessageContent ( ) . then ( function ( message ) {
2014-05-09 06:00:49 +00:00
if ( session . pendingPreKey !== undefined ) {
preKeyMsg . baseKey = toArrayBuffer ( session . pendingPreKey ) ;
2014-05-13 05:51:46 +00:00
preKeyMsg . message = message ;
2014-05-09 06:00:49 +00:00
var result = String . fromCharCode ( ( 2 << 4 ) | 2 ) + getString ( preKeyMsg . encode ( ) ) ;
2014-05-09 07:20:54 +00:00
return { type : 3 , body : result } ;
2014-05-09 06:00:49 +00:00
} else
2014-05-09 07:20:54 +00:00
return { type : 1 , body : getString ( message ) } ;
2014-05-09 06:00:49 +00:00
} ) ;
}
var GENERATE _KEYS _KEYS _GENERATED = 100 ;
2014-05-17 05:53:58 +00:00
self . generateKeys = function ( ) {
2014-05-09 06:00:49 +00:00
var identityKey = crypto _storage . getStoredPubKey ( "identityKey" ) ;
var identityKeyCalculated = function ( pubKey ) {
identityKey = pubKey ;
2014-05-21 02:21:07 +00:00
var firstKeyId = textsecure . storage . getEncrypted ( "maxPreKeyId" , - 1 ) + 1 ;
textsecure . storage . putEncrypted ( "maxPreKeyId" , firstKeyId + GENERATE _KEYS _KEYS _GENERATED ) ;
2014-05-09 06:00:49 +00:00
if ( firstKeyId > 16777000 )
return new Promise ( function ( ) { throw new Error ( "You crazy motherfucker" ) } ) ;
var keys = { } ;
keys . keys = [ ] ;
2014-05-13 08:40:29 +00: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 06:00:49 +00:00
} ) ;
2014-05-13 08:40:29 +00:00
} else
return keys ;
2014-05-09 06:00:49 +00:00
} ) ;
}
if ( identityKey === undefined )
return crypto _storage . getNewPubKeySTORINGPrivKey ( "identityKey" , true ) . then ( function ( pubKey ) { return identityKeyCalculated ( pubKey ) ; } ) ;
else
return identityKeyCalculated ( identityKey ) ;
}
2014-05-17 04:54:12 +00:00
2014-05-17 05:53:58 +00:00
self . testing _only = testing _only ;
return self ;
2014-05-17 04:54:12 +00:00
} ( ) ;