2014-05-14 20:58:12 +02:00
/ * v i m : t s = 4 : s w = 4
*
2014-05-04 08:34:13 +02:00
* 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-04-04 08:33:02 +02:00
/* START CRAP TO BE DELETED */
2014-06-06 04:34:01 +02:00
//Stolen from MDN (TODO: replace with something else so we arent infringing their copyright)
2014-01-12 08:32:13 +01:00
function b64ToUint6 ( nChr ) {
return nChr > 64 && nChr < 91 ?
nChr - 65
: nChr > 96 && nChr < 123 ?
nChr - 71
: nChr > 47 && nChr < 58 ?
nChr + 4
: nChr === 43 ?
62
: nChr === 47 ?
63
:
0 ;
}
function base64DecToArr ( sBase64 , nBlocksSize ) {
var
sB64Enc = sBase64 . replace ( /[^A-Za-z0-9\+\/]/g , "" ) , nInLen = sB64Enc . length ,
2014-01-22 07:23:41 +01:00
nOutLen = nBlocksSize ? Math . ceil ( ( nInLen * 3 + 1 >> 2 ) / nBlocksSize ) * nBlocksSize : nInLen * 3 + 1 >> 2 ;
var aBBytes = new ArrayBuffer ( nOutLen ) ;
var taBytes = new Uint8Array ( aBBytes ) ;
2014-01-12 08:32:13 +01:00
for ( var nMod3 , nMod4 , nUint24 = 0 , nOutIdx = 0 , nInIdx = 0 ; nInIdx < nInLen ; nInIdx ++ ) {
nMod4 = nInIdx & 3 ;
nUint24 |= b64ToUint6 ( sB64Enc . charCodeAt ( nInIdx ) ) << 18 - 6 * nMod4 ;
if ( nMod4 === 3 || nInLen - nInIdx === 1 ) {
for ( nMod3 = 0 ; nMod3 < 3 && nOutIdx < nOutLen ; nMod3 ++ , nOutIdx ++ ) {
taBytes [ nOutIdx ] = nUint24 >>> ( 16 >>> nMod3 & 24 ) & 255 ;
}
nUint24 = 0 ;
}
}
2014-01-22 07:23:41 +01:00
return aBBytes ;
2014-01-12 08:32:13 +01:00
}
2014-03-05 02:31:15 +01:00
/* Base64 string to array encoding */
function uint6ToB64 ( nUint6 ) {
return nUint6 < 26 ?
nUint6 + 65
: nUint6 < 52 ?
nUint6 + 71
: nUint6 < 62 ?
nUint6 - 4
: nUint6 === 62 ?
43
: nUint6 === 63 ?
47
:
65 ;
}
function base64EncArr ( aBytes ) {
var nMod3 , sB64Enc = "" ;
for ( var nLen = aBytes . length , nUint24 = 0 , nIdx = 0 ; nIdx < nLen ; nIdx ++ ) {
nMod3 = nIdx % 3 ;
//if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; }
nUint24 |= aBytes [ nIdx ] << ( 16 >>> nMod3 & 24 ) ;
if ( nMod3 === 2 || aBytes . length - nIdx === 1 ) {
sB64Enc += String . fromCharCode ( uint6ToB64 ( nUint24 >>> 18 & 63 ) , uint6ToB64 ( nUint24 >>> 12 & 63 ) , uint6ToB64 ( nUint24 >>> 6 & 63 ) , uint6ToB64 ( nUint24 & 63 ) ) ;
nUint24 = 0 ;
}
}
return sB64Enc . replace ( /A(?=A$|$)/g , "=" ) ;
}
2014-04-04 08:33:02 +02:00
/* END CRAP TO BE DELETED */
2014-05-17 07:53:58 +02:00
window . textsecure = window . textsecure || { } ;
2014-01-12 08:32:13 +01:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Type conversion utilities * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
2014-01-15 08:46:05 +01:00
// Strings/arrays
2014-03-06 22:44:59 +01:00
//TODO: Throw all this shit in favor of consistent types
2014-07-21 04:06:04 +02:00
//TODO: Namespace
2014-01-12 15:07:13 +01:00
var StaticByteBufferProto = new dcodeIO . ByteBuffer ( ) . _ _proto _ _ ;
2014-01-22 07:23:41 +01:00
var StaticArrayBufferProto = new ArrayBuffer ( ) . _ _proto _ _ ;
2014-03-06 19:18:11 +01:00
var StaticUint8ArrayProto = new Uint8Array ( ) . _ _proto _ _ ;
var StaticWordArrayProto = CryptoJS . lib . WordArray . create ( '' ) . _ _proto _ _ ;
2014-01-12 08:32:13 +01:00
function getString ( thing ) {
2014-03-06 19:18:11 +01:00
if ( thing === Object ( thing ) ) {
if ( thing . _ _proto _ _ == StaticUint8ArrayProto )
return String . fromCharCode . apply ( null , thing ) ;
if ( thing . _ _proto _ _ == StaticArrayBufferProto )
return getString ( new Uint8Array ( thing ) ) ;
if ( thing . _ _proto _ _ == StaticByteBufferProto )
return thing . toString ( "binary" ) ;
if ( thing . _ _proto _ _ == StaticWordArrayProto )
return thing . toString ( CryptoJS . enc . Latin1 ) ;
}
2014-01-12 08:32:13 +01:00
return thing ;
}
2014-03-05 02:31:15 +01:00
function getStringable ( thing ) {
2014-03-10 23:47:37 +01:00
return ( typeof thing == "string" || typeof thing == "number" || typeof thing == "boolean" ||
2014-03-06 19:18:11 +01:00
( thing === Object ( thing ) &&
( thing . _ _proto _ _ == StaticArrayBufferProto ||
thing . _ _proto _ _ == StaticUint8ArrayProto ||
thing . _ _proto _ _ == StaticByteBufferProto ||
thing . _ _proto _ _ == StaticWordArrayProto ) ) ) ;
2014-03-05 02:31:15 +01:00
}
2014-07-21 04:55:07 +02:00
function isEqual ( a , b ) {
2014-07-21 04:06:04 +02:00
// TODO: Special-case arraybuffers, etc
a = getString ( a ) ;
b = getString ( b ) ;
2014-07-21 04:55:07 +02:00
var maxLength = Math . min ( a . length , b . length ) ;
if ( maxLength < 5 )
throw new Error ( "a/b compare too short" ) ;
2014-07-21 04:06:04 +02:00
return a . substring ( 0 , Math . min ( maxLength , a . length ) ) == b . substring ( 0 , Math . min ( maxLength , b . length ) ) ;
}
2014-03-05 02:31:15 +01:00
function toArrayBuffer ( thing ) {
2014-03-06 19:18:11 +01:00
//TODO: Optimize this for specific cases
2014-03-05 02:31:15 +01:00
if ( thing === undefined )
return undefined ;
2014-03-06 22:44:59 +01:00
if ( thing === Object ( thing ) && thing . _ _proto _ _ == StaticArrayBufferProto )
return thing ;
2014-03-10 01:32:00 +01:00
if ( thing instanceof Array ) {
// Assuming Uint16Array from curve25519
var res = new ArrayBuffer ( thing . length * 2 ) ;
var uint = new Uint16Array ( res ) ;
for ( var i = 0 ; i < thing . length ; i ++ )
uint [ i ] = thing [ i ] ;
return res ;
}
2014-03-05 02:31:15 +01:00
if ( ! getStringable ( thing ) )
2014-05-03 05:16:36 +02:00
throw new Error ( "Tried to convert a non-stringable thing of type " + typeof thing + " to an array buffer" ) ;
2014-03-05 02:31:15 +01:00
var str = getString ( thing ) ;
var res = new ArrayBuffer ( str . length ) ;
var uint = new Uint8Array ( res ) ;
for ( var i = 0 ; i < str . length ; i ++ )
uint [ i ] = str . charCodeAt ( i ) ;
return res ;
}
2014-01-22 07:23:41 +01:00
function base64ToArrayBuffer ( string ) {
2014-01-12 08:32:13 +01:00
return base64DecToArr ( string ) ;
}
2014-05-17 07:53:58 +02:00
// Protobuf decoding
2014-01-15 08:46:05 +01:00
//TODO: throw on missing fields everywhere
2014-05-21 21:04:05 +02:00
window . textsecure . protos = function ( ) {
var self = { } ;
2014-01-15 08:46:05 +01:00
2014-05-21 21:04:05 +02:00
self . IncomingPushMessageProtobuf = dcodeIO . ProtoBuf . loadProtoFile ( "protos/IncomingPushMessageSignal.proto" ) . build ( "textsecure.IncomingPushMessageSignal" ) ;
self . decodeIncomingPushMessageProtobuf = function ( string ) {
2014-05-26 01:48:41 +02:00
return self . IncomingPushMessageProtobuf . decode ( btoa ( string ) ) ;
2014-05-21 21:04:05 +02:00
}
2014-01-15 08:46:05 +01:00
2014-05-21 21:04:05 +02:00
self . PushMessageContentProtobuf = dcodeIO . ProtoBuf . loadProtoFile ( "protos/IncomingPushMessageSignal.proto" ) . build ( "textsecure.PushMessageContent" ) ;
self . decodePushMessageContentProtobuf = function ( string ) {
2014-05-26 01:48:41 +02:00
return self . PushMessageContentProtobuf . decode ( btoa ( string ) ) ;
2014-05-21 21:04:05 +02:00
}
2014-01-15 08:46:05 +01:00
2014-05-21 21:04:05 +02:00
self . WhisperMessageProtobuf = dcodeIO . ProtoBuf . loadProtoFile ( "protos/WhisperTextProtocol.proto" ) . build ( "textsecure.WhisperMessage" ) ;
self . decodeWhisperMessageProtobuf = function ( string ) {
2014-05-26 01:48:41 +02:00
return self . WhisperMessageProtobuf . decode ( btoa ( string ) ) ;
2014-05-21 21:04:05 +02:00
}
2014-01-15 08:46:05 +01:00
2014-05-21 21:04:05 +02:00
self . PreKeyWhisperMessageProtobuf = dcodeIO . ProtoBuf . loadProtoFile ( "protos/WhisperTextProtocol.proto" ) . build ( "textsecure.PreKeyWhisperMessage" ) ;
self . decodePreKeyWhisperMessageProtobuf = function ( string ) {
2014-05-26 01:48:41 +02:00
return self . PreKeyWhisperMessageProtobuf . decode ( btoa ( string ) ) ;
2014-05-21 21:04:05 +02:00
}
self . KeyExchangeMessageProtobuf = dcodeIO . ProtoBuf . loadProtoFile ( "protos/WhisperTextProtocol.proto" ) . build ( "textsecure.KeyExchangeMessage" ) ;
self . decodeKeyExchangeMessageProtobuf = function ( string ) {
2014-05-26 01:48:41 +02:00
return self . KeyExchangeMessageProtobuf . decode ( btoa ( string ) ) ;
2014-05-21 21:04:05 +02:00
}
return self ;
} ( ) ;
2014-01-12 08:32:13 +01:00
2014-05-26 00:09:07 +02:00
// Number formatting utils
window . textsecure . utils = function ( ) {
var self = { } ;
2014-05-26 00:45:22 +02:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Number conversion / checking stuff * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
2014-06-30 00:15:27 +02:00
self . getAllRegionCodes = function ( ) {
return { "AD" : "Andorra" , "AE" : "United Arab Emirates" , "AF" : "Afghanistan" , "AG" : "Antigua and Barbuda" , "AI" : "Anguilla" , "AL" : "Albania" , "AM" : "Armenia" , "AO" : "Angola" , "AR" : "Argentina" , "AS" : "AmericanSamoa" , "AT" : "Austria" , "AU" : "Australia" , "AW" : "Aruba" , "AX" : "Åland Islands" , "AZ" : "Azerbaijan" , "BA" : "Bosnia and Herzegovina" , "BB" : "Barbados" , "BD" : "Bangladesh" , "BE" : "Belgium" , "BF" : "Burkina Faso" , "BG" : "Bulgaria" , "BH" : "Bahrain" , "BI" : "Burundi" , "BJ" : "Benin" , "BL" : "Saint Barthélemy" , "BM" : "Bermuda" , "BN" : "Brunei Darussalam" , "BO" : "Bolivia, Plurinational State of" , "BR" : "Brazil" , "BS" : "Bahamas" , "BT" : "Bhutan" , "BW" : "Botswana" , "BY" : "Belarus" , "BZ" : "Belize" , "CA" : "Canada" , "CC" : "Cocos (Keeling) Islands" , "CD" : "Congo, The Democratic Republic of the" , "CF" : "Central African Republic" , "CG" : "Congo" , "CH" : "Switzerland" , "CI" : "Cote d'Ivoire" , "CK" : "Cook Islands" , "CL" : "Chile" , "CM" : "Cameroon" , "CN" : "China" , "CO" : "Colombia" , "CR" : "Costa Rica" , "CU" : "Cuba" , "CV" : "Cape Verde" , "CX" : "Christmas Island" , "CY" : "Cyprus" , "CZ" : "Czech Republic" , "DE" : "Germany" , "DJ" : "Djibouti" , "DK" : "Denmark" , "DM" : "Dominica" , "DO" : "Dominican Republic" , "DZ" : "Algeria" , "EC" : "Ecuador" , "EE" : "Estonia" , "EG" : "Egypt" , "ER" : "Eritrea" , "ES" : "Spain" , "ET" : "Ethiopia" , "FI" : "Finland" , "FJ" : "Fiji" , "FK" : "Falkland Islands (Malvinas)" , "FM" : "Micronesia, Federated States of" , "FO" : "Faroe Islands" , "FR" : "France" , "GA" : "Gabon" , "GB" : "United Kingdom" , "GD" : "Grenada" , "GE" : "Georgia" , "GF" : "French Guiana" , "GG" : "Guernsey" , "GH" : "Ghana" , "GI" : "Gibraltar" , "GL" : "Greenland" , "GM" : "Gambia" , "GN" : "Guinea" , "GP" : "Guadeloupe" , "GQ" : "Equatorial Guinea" , "GR" : "Ελλάδα" , "GT" : "Guatemala" , "GU" : "Guam" , "GW" : "Guinea-Bissau" , "GY" : "Guyana" , "HK" : "Hong Kong" , "HN" : "Honduras" , "HR" : "Croatia" , "HT" : "Haiti" , "HU" : "Magyarország" , "ID" : "Indonesia" , "IE" : "Ireland" , "IL" : "Israel" , "IM" : "Isle of Man" , "IN" : "India" , "IO" : "British Indian Ocean Territory" , "IQ" : "Iraq" , "IR" : "Iran, Islamic Republic of" , "IS" : "Iceland" , "IT" : "Italy" , "JE" : "Jersey" , "JM" : "Jamaica" , "JO" : "Jordan" , "JP" : "Japan" , "KE" : "Kenya" , "KG" : "Kyrgyzstan" , "KH" : "Cambodia" , "KI" : "Kiribati" , "KM" : "Comoros" , "KN" : "Saint Kitts and Nevis" , "KP" : "Korea, Democratic People's Republic of" , "KR" : "Korea, Republic of" , "KW" : "Kuwait" , "KY" : "Cayman Islands" , "KZ" : "Kazakhstan" , "LA" : "Lao People's Democratic Republic" , "LB" : "Lebanon" , "LC" : "Saint Lucia" , "LI" : "Liechtenstein" , "LK" : "Sri Lanka" , "LR" : "Liberia" , "LS" : "Lesotho" , "LT" : "Lithuania" , "LU" : "Luxembourg" , "LV" : "Latvia" , "LY" : "Libyan Arab Jamahiriya" , "MA" : "Morocco" , "MC" : "Monaco" , "MD" : "Moldova, Republic of" , "ME" : "Црна Гора" , "MF" : "Saint Martin" , "MG" : "Madagascar" , "MH" : "Marshall Islands" , "MK" : "Macedonia, The Former Yugoslav Republic of" , "ML" : "Mali" , "MM" : "Myanmar" , "MN" : "Mongolia" , "MO" : "Macao" , "MP" : "Northern Mariana Islands" , "MQ" : "Martinique" , "MR" : "Mauritania" , "MS" : "Montserrat" , "MT" : "Malta" , "MU" : "Mauritius" , "MV" : "Maldives" , "MW" : "Malawi" , "MX" : "Mexico" , "MY" : "Malaysia" , "MZ" : "Mozambique" , "NA" : "Namibia" , "NC" : "New Caledonia" , "NE" : "Niger" , "NF" : "Norfolk Island" , "NG" : "Nigeria" , "NI" : "Nicaragua" , "NL" : "Netherlands" , "NO" : "Norway" , "NP" : "Nepal" , "NR" : "Nauru" , "NU" : "Niue" , "NZ" : "New Zealand" , "OM" : "Oman" , "PA" : "Panama" , "PE" : "Peru" , "PF" : "French Polynesia" , "PG" : "Papua New Guinea" , "PH" : "Philippines" , "PK" : "Pakistan" , "PL" : "Polska" , "PM" : "Saint Pierre and Miquelon" , "PR" : "Puerto Rico" , "PS" : "Palestinian Territory, Occupied" , "PT" : "Portugal" , "PW" : "Palau" , "PY" : "Paraguay" , "QA" : "Qatar" , "RE" : "Réunion" , "RO" : "Romania" , "RS" : "Србија" , "RU" : "Russia" , "RW" : "Rwanda" , "SA" : "Saudi Arabia" , "SB" : "Solomon Islands" , "SC" : "Seychelles" , "SD" : "Sudan" , "SE" : "Sweden" , "SG" : "Singapore" , "SH" : "Saint Helena, Ascension and Tristan Da Cunha" , "SI" : "Slovenia" , "SJ" : "Svalbard and Jan Mayen" , "SK" : "Slovakia" , "SL" : "Sierra Leone" , "SM" : "San Marino" , "SN" : "Senegal" , "SO" : "Somalia" , "SR" : "Suriname" , "ST" : "Sao Tome and Principe" , "SV" : "El Salvador" , "SY" : "Syrian Arab Republic" , "SZ" : "Swaziland" , "TC" : "Turks and Caicos Islands" , "TD" : "Chad" , "TG" : "Togo" , "TH" : "Thailand" , "TJ" : "Tajikistan" , "TK" : "Tokelau" , "TL" : "Timor-Leste" , "TM" : "Turkmenistan" , "TN" : "Tunisia" , "TO" : "Tonga" , "TR" : "Turkey" , "TT" : "Trinidad and Tobago" , "TV" : "Tuvalu" , "TW" : "Taiwan, Province of China" , "TZ" : "Tanzania, United Republic of"
} ;
self . getRegionCodeForCountryCode = function ( countryCode ) {
return libphonenumber . getRegionCodeForCountryCode ( countryCode ) ;
} ;
self . getRegionCodeForNumber = function ( number ) {
try {
var parsedNumber = libphonenumber . parse ( number ) ;
return libphonenumber . getRegionCodeForNumber ( parsedNumber ) ;
} catch ( e ) {
return "ZZ" ;
}
} ;
2014-05-26 00:09:07 +02:00
2014-06-30 00:15:27 +02:00
self . getCountryCodeForRegion = function ( regionCode ) {
var cc = libphonenumber . getCountryCodeForRegion ( regionCode ) ;
return ( cc != 0 ) ? cc : "" ;
} ;
2014-05-26 00:09:07 +02:00
2014-06-30 00:15:27 +02:00
self . verifyNumber = function ( number , regionCode ) {
var parsedNumber = libphonenumber . parse ( number , regionCode ) ;
2014-05-26 00:09:07 +02:00
2014-06-30 00:15:27 +02:00
if ( ! regionCode || regionCode == 'ZZ' )
regionCode = libphonenumber . getRegionCodeForNumber ( parsedNumber ) ;
2014-05-26 00:09:07 +02:00
2014-06-30 00:15:27 +02:00
var isValidNumber = libphonenumber . isValidNumber ( parsedNumber ) ;
var isValidNumberForRegion = libphonenumber . isValidNumberForRegion ( parsedNumber , regionCode ) ;
if ( isValidNumber && isValidNumberForRegion ) {
return libphonenumber . format ( parsedNumber , libphonenumber . PhoneNumberFormat . E164 ) ;
} else {
throw new Error ( "The number seems not to be valid." ) ;
}
} ;
2014-05-26 00:09:07 +02:00
2014-05-26 00:45:22 +02:00
self . unencodeNumber = function ( number ) {
2014-05-31 19:28:46 +02:00
return number . split ( "." ) ;
2014-06-30 00:15:27 +02:00
} ;
2014-05-26 00:09:07 +02:00
2014-05-26 00:45:22 +02:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * JSON ' ing Utilities * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * /
2014-05-21 21:04:05 +02:00
function ensureStringed ( thing ) {
if ( getStringable ( thing ) )
return getString ( thing ) ;
else if ( thing instanceof Array ) {
var res = [ ] ;
for ( var i = 0 ; i < thing . length ; i ++ )
res [ i ] = ensureStringed ( thing [ i ] ) ;
return res ;
} else if ( thing === Object ( thing ) ) {
var res = { } ;
for ( key in thing )
res [ key ] = ensureStringed ( thing [ key ] ) ;
return res ;
}
throw new Error ( "unsure of how to jsonify object of type " + typeof thing ) ;
}
2014-05-26 00:45:22 +02:00
self . jsonThing = function ( thing ) {
2014-05-21 21:04:05 +02:00
return JSON . stringify ( ensureStringed ( thing ) ) ;
}
2014-05-26 00:45:22 +02:00
return self ;
} ( ) ;
2014-05-27 21:29:44 +02:00
window . textsecure . throwHumanError = function ( error , type , humanError ) {
var e = new Error ( error ) ;
if ( type !== undefined )
e . name = type ;
e . humanError = humanError ;
throw e ;
}
2014-05-26 00:45:22 +02:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Utilities to store data in local storage * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
window . textsecure . storage = function ( ) {
var self = { } ;
2014-05-17 06:54:12 +02:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * Base Storage Routines * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
2014-05-17 07:53:58 +02:00
self . putEncrypted = function ( key , value ) {
2014-05-17 06:54:12 +02:00
//TODO
if ( value === undefined )
throw new Error ( "Tried to store undefined" ) ;
2014-05-26 00:45:22 +02:00
localStorage . setItem ( "e" + key , textsecure . utils . jsonThing ( value ) ) ;
2014-05-17 06:54:12 +02:00
}
2014-01-17 07:08:33 +01:00
2014-05-17 07:53:58 +02:00
self . getEncrypted = function ( key , defaultValue ) {
2014-01-10 08:48:05 +01:00
//TODO
2014-05-17 06:54:12 +02:00
var value = localStorage . getItem ( "e" + key ) ;
if ( value === null )
return defaultValue ;
return JSON . parse ( value ) ;
}
2014-01-10 08:48:05 +01:00
2014-05-17 07:53:58 +02:00
self . removeEncrypted = function ( key ) {
2014-05-17 06:54:12 +02:00
localStorage . removeItem ( "e" + key ) ;
}
2014-01-10 08:48:05 +01:00
2014-05-17 07:53:58 +02:00
self . putUnencrypted = function ( key , value ) {
2014-05-17 06:54:12 +02:00
if ( value === undefined )
throw new Error ( "Tried to store undefined" ) ;
2014-05-26 00:45:22 +02:00
localStorage . setItem ( "u" + key , textsecure . utils . jsonThing ( value ) ) ;
2014-05-17 06:54:12 +02:00
}
2014-01-15 08:46:05 +01:00
2014-05-17 07:53:58 +02:00
self . getUnencrypted = function ( key , defaultValue ) {
2014-05-17 06:54:12 +02:00
var value = localStorage . getItem ( "u" + key ) ;
if ( value === null )
return defaultValue ;
return JSON . parse ( value ) ;
}
2014-01-10 08:48:05 +01:00
2014-05-17 07:53:58 +02:00
self . removeUnencrypted = function ( key ) {
2014-05-17 06:54:12 +02:00
localStorage . removeItem ( "u" + key ) ;
}
2014-01-10 08:48:05 +01:00
2014-05-17 06:54:12 +02:00
/ * * * * * * * * * * * * * * * * * * * * * *
* * * Device Storage * * *
* * * * * * * * * * * * * * * * * * * * * * /
2014-05-17 07:53:58 +02:00
self . devices = function ( ) {
var self = { } ;
2014-05-28 01:14:16 +02:00
self . saveDeviceObject = function ( deviceObject ) {
2014-05-31 19:28:46 +02:00
if ( deviceObject . identityKey === undefined || deviceObject . registrationId === undefined || deviceObject . encodedNumber === undefined )
throw new Error ( "Tried to store invalid deviceObject" ) ;
var number = textsecure . utils . unencodeNumber ( deviceObject . encodedNumber ) [ 0 ] ;
2014-05-28 01:14:16 +02:00
var map = textsecure . storage . getEncrypted ( "devices" + number ) ;
2014-05-17 06:54:12 +02:00
2014-05-28 01:14:16 +02:00
if ( map === undefined )
map = { devices : [ deviceObject ] , identityKey : deviceObject . identityKey } ;
else if ( map . identityKey != getString ( deviceObject . identityKey ) )
throw new Error ( "Identity key changed" ) ;
2014-05-31 19:28:46 +02:00
else {
var updated = false ;
for ( i in map . devices ) {
if ( map . devices [ i ] . encodedNumber == deviceObject . encodedNumber ) {
map . devices [ i ] = deviceObject ;
updated = true ;
}
}
if ( ! updated )
map . devices . push ( deviceObject ) ;
}
2014-05-17 06:54:12 +02:00
2014-05-28 01:14:16 +02:00
textsecure . storage . putEncrypted ( "devices" + number , map ) ;
2014-05-17 06:54:12 +02:00
}
2014-05-28 01:14:16 +02:00
self . getDeviceObjectsForNumber = function ( number ) {
var map = textsecure . storage . getEncrypted ( "devices" + number ) ;
return map === undefined ? [ ] : map . devices ;
2014-05-17 07:53:58 +02:00
}
2014-05-31 19:28:46 +02:00
self . getDeviceObject = function ( encodedNumber ) {
var number = textsecure . utils . unencodeNumber ( encodedNumber ) ;
var devices = self . getDeviceObjectsForNumber ( number [ 0 ] ) ;
if ( devices === undefined )
return undefined ;
for ( i in devices )
if ( devices [ i ] . encodedNumber == encodedNumber )
return devices [ i ] ;
return undefined ;
}
2014-05-28 01:14:16 +02:00
self . removeDeviceIdsForNumber = function ( number , deviceIdsToRemove ) {
var map = textsecure . storage . getEncrypted ( "devices" + number ) ;
if ( map === undefined )
throw new Error ( "Tried to remove device for unknown number" ) ;
var newDevices = [ ] ;
var devicesRemoved = 0 ;
2014-05-28 04:16:45 +02:00
for ( i in map . devices ) {
2014-05-28 01:14:16 +02:00
var keep = true ;
2014-06-01 19:39:35 +02:00
for ( j in deviceIdsToRemove )
if ( map . devices [ i ] . encodedNumber == number + "." + deviceIdsToRemove [ j ] )
2014-05-28 01:14:16 +02:00
keep = false ;
if ( keep )
2014-05-28 04:16:45 +02:00
newDevices . push ( map . devices [ i ] ) ;
2014-05-28 01:14:16 +02:00
else
devicesRemoved ++ ;
2014-05-17 06:54:12 +02:00
}
2014-05-28 01:14:16 +02:00
if ( devicesRemoved != deviceIdsToRemove . length )
throw new Error ( "Tried to remove unknown device" ) ;
2014-05-17 06:54:12 +02:00
}
2014-05-17 07:53:58 +02:00
return self ;
} ( ) ;
2014-06-03 18:39:29 +02:00
/ * * * * * * * * * * * * * * * * * * * * *
* * * Group Storage * * *
* * * * * * * * * * * * * * * * * * * * * /
self . groups = function ( ) {
var self = { } ;
2014-06-06 03:05:42 +02:00
var addGroupToNumber = function ( groupId , number ) {
var membership = textsecure . storage . getEncrypted ( "groupMembership" + number , [ groupId ] ) ;
if ( membership . indexOf ( groupId ) < 0 )
membership . push ( groupId ) ;
textsecure . storage . putEncrypted ( "groupMembership" + number , membership ) ;
}
var removeGroupFromNumber = function ( groupId , number ) {
var membership = textsecure . storage . getEncrypted ( "groupMembership" + number , [ groupId ] ) ;
membership = membership . filter ( function ( group ) { return group != groupId ; } ) ;
if ( membership . length == 0 )
textsecure . storage . removeEncrypted ( "groupMembership" + number ) ;
else
textsecure . storage . putEncrypted ( "groupMembership" + number , membership ) ;
}
2014-06-06 04:26:01 +02:00
self . getGroupListForNumber = function ( number ) {
return textsecure . storage . getEncrypted ( "groupMembership" + number , [ ] ) ;
}
2014-06-04 03:09:04 +02:00
self . createNewGroup = function ( numbers , groupId ) {
if ( groupId === undefined ) {
while ( textsecure . storage . getEncrypted ( "group" + groupId ) !== undefined )
groupId = new Uint32Array ( textsecure . crypto . getRandomBytes ( 4 ) ) [ 0 ] ;
2014-06-06 03:05:42 +02:00
} else if ( textsecure . storage . getEncrypted ( "group" + groupId ) !== undefined )
throw new Error ( "Tried to recreate group" ) ;
2014-06-04 03:09:04 +02:00
2014-06-06 01:20:09 +02:00
var me = textsecure . utils . unencodeNumber ( textsecure . storage . getUnencrypted ( "number_id" ) ) [ 0 ] ;
var haveMe = false ;
var finalNumbers = [ ] ;
for ( i in numbers ) {
var number = textsecure . utils . verifyNumber ( numbers [ i ] ) ;
if ( number == me )
haveMe = true ;
2014-06-06 03:05:42 +02:00
if ( finalNumbers . indexOf ( number ) < 0 ) {
2014-06-06 01:20:09 +02:00
finalNumbers . push ( number ) ;
2014-06-06 03:05:42 +02:00
addGroupToNumber ( groupId , number ) ;
}
2014-06-06 01:20:09 +02:00
}
if ( ! haveMe )
finalNumbers . push ( me ) ;
textsecure . storage . putEncrypted ( "group" + groupId , { numbers : finalNumbers } ) ;
2014-06-04 03:09:04 +02:00
2014-06-06 01:20:09 +02:00
return { id : groupId , numbers : finalNumbers } ;
2014-06-04 03:09:04 +02:00
}
2014-06-03 18:39:29 +02:00
self . getNumbers = function ( groupId ) {
2014-06-04 03:09:04 +02:00
var group = textsecure . storage . getEncrypted ( "group" + groupId ) ;
if ( group === undefined )
return undefined ;
return group . numbers ;
}
2014-06-04 04:18:14 +02:00
self . removeNumber = function ( groupId , number ) {
var group = textsecure . storage . getEncrypted ( "group" + groupId ) ;
if ( group === undefined )
return undefined ;
2014-06-06 01:20:09 +02:00
try {
number = textsecure . utils . verifyNumber ( number ) ;
} catch ( e ) {
return group . numbers ;
}
var me = textsecure . utils . unencodeNumber ( textsecure . storage . getUnencrypted ( "number_id" ) ) [ 0 ] ;
if ( number == me )
throw new Error ( "Cannot remove ourselves from a group, leave the group instead" ) ;
var i = group . numbers . indexOf ( number ) ;
if ( i > - 1 ) {
group . numbers . slice ( i , 1 ) ;
textsecure . storage . putEncrypted ( "group" + groupId , group ) ;
2014-06-06 03:05:42 +02:00
removeGroupFromNumber ( groupId , number ) ;
2014-06-06 01:20:09 +02:00
}
2014-06-04 04:18:14 +02:00
return group . numbers ;
}
self . addNumbers = function ( groupId , numbers ) {
var group = textsecure . storage . getEncrypted ( "group" + groupId ) ;
if ( group === undefined )
return undefined ;
2014-06-06 01:20:09 +02:00
for ( i in numbers ) {
var number = textsecure . utils . verifyNumber ( numbers [ i ] ) ;
2014-06-06 04:26:01 +02:00
if ( group . numbers . indexOf ( number ) < 0 ) {
2014-06-06 01:20:09 +02:00
group . numbers . push ( number ) ;
2014-06-06 04:26:01 +02:00
addGroupToNumber ( groupId , number ) ;
}
2014-06-06 01:20:09 +02:00
}
2014-06-04 04:18:14 +02:00
textsecure . storage . putEncrypted ( "group" + groupId , group ) ;
return group . numbers ;
}
2014-06-04 03:09:04 +02:00
self . deleteGroup = function ( groupId ) {
textsecure . storage . removeEncrypted ( "group" + groupId ) ;
2014-06-03 18:39:29 +02:00
}
2014-06-06 04:26:01 +02:00
self . getGroup = function ( groupId ) {
var group = textsecure . storage . getEncrypted ( "group" + groupId ) ;
if ( group === undefined )
return undefined ;
return { id : groupId , numbers : group . numbers } ; //TODO: avatar/name tracking
}
2014-06-03 18:39:29 +02:00
return self ;
} ( ) ;
2014-05-17 07:53:58 +02:00
return self ;
} ( ) ;
2014-01-15 08:46:05 +01:00
2014-01-22 07:23:41 +01:00
/ * * * * * * * * * * * * * * * * * * * * * *
* * * NaCL Interface * * *
* * * * * * * * * * * * * * * * * * * * * * /
2014-05-17 07:53:58 +02:00
window . textsecure . nacl = function ( ) {
var self = { } ;
2014-07-20 22:31:56 +02:00
self . USE _NACL = true ;
2014-05-17 07:53:58 +02:00
var onLoadCallbacks = [ ] ;
var naclLoaded = 0 ;
self . registerOnLoadFunction = function ( func ) {
if ( naclLoaded || ! self . USE _NACL ) {
func ( ) ;
return ;
}
onLoadCallbacks [ onLoadCallbacks . length ] = func ;
2014-03-10 01:32:00 +01:00
}
2014-01-22 07:23:41 +01:00
2014-05-17 07:53:58 +02:00
var naclMessageNextId = 0 ;
var naclMessageIdCallbackMap = { } ;
window . moduleDidLoad = function ( ) {
common . hideModule ( ) ;
naclLoaded = 1 ;
for ( var i = 0 ; i < onLoadCallbacks . length ; i ++ )
onLoadCallbacks [ i ] ( ) ;
onLoadCallbacks = [ ] ;
}
2014-01-22 07:23:41 +01:00
2014-05-17 07:53:58 +02:00
window . handleMessage = function ( message ) {
naclMessageIdCallbackMap [ message . data . call _id ] ( message . data ) ;
}
2014-01-22 07:23:41 +01:00
2014-05-17 07:53:58 +02:00
self . postNaclMessage = function ( message ) {
if ( ! self . USE _NACL )
throw new Error ( "Attempted to make NaCL call with !USE_NACL?" ) ;
2014-03-10 01:32:00 +01:00
2014-05-17 07:53:58 +02:00
return new Promise ( function ( resolve ) {
naclMessageIdCallbackMap [ naclMessageNextId ] = resolve ;
message . call _id = naclMessageNextId ++ ;
2014-01-15 08:46:05 +01:00
2014-05-17 07:53:58 +02:00
common . naclModule . postMessage ( message ) ;
} ) ;
}
return self ;
} ( ) ;
2014-01-10 08:48:05 +01:00
2014-05-17 07:53:58 +02:00
//TODO: Some kind of textsecure.init(use_nacl)
window . textsecure . registerOnLoadFunction = window . textsecure . nacl . registerOnLoadFunction ;
2014-06-01 19:39:35 +02:00
window . textsecure . replay = function ( ) {
var self = { } ;
self . REPLAY _FUNCS = {
SEND _MESSAGE : 1 ,
INIT _SESSION : 2 ,
}
var functions = { } ;
self . registerReplayFunction = function ( func , functionCode ) {
functions [ functionCode ] = func ;
}
self . replayError = function ( replayData ) {
var args = Array . prototype . slice . call ( arguments ) ;
args . shift ( ) ;
args = replayData . args . concat ( args ) ;
functions [ replayData . replayFunction ] . apply ( window , args ) ;
}
self . createReplayableError = function ( shortMsg , longMsg , replayFunction , args ) {
var e = new Error ( shortMsg ) ;
e . name = "ReplayableError" ;
e . humanError = e . longMessage = longMsg ;
e . replayData = { replayFunction : replayFunction , args : args } ;
e . replay = function ( ) {
self . replayError ( e . replayData ) ;
}
return e ;
}
return self ;
} ( ) ;
2014-05-17 07:53:58 +02:00
// message_callback({message: decryptedMessage, pushMessage: server-providedPushMessage})
window . textsecure . subscribeToPush = function ( ) {
var subscribeToPushMessageSemaphore = 0 ;
return function ( message _callback ) {
subscribeToPushMessageSemaphore ++ ;
if ( subscribeToPushMessageSemaphore <= 0 )
2014-03-25 20:27:19 +01:00
return ;
2014-05-17 22:56:08 +02:00
var socket = textsecure . api . getWebsocket ( ) ;
2014-05-17 07:53:58 +02:00
var pingInterval ;
//TODO: GUI
socket . onerror = function ( socketEvent ) {
console . log ( 'Server is down :(' ) ;
clearInterval ( pingInterval ) ;
subscribeToPushMessageSemaphore -- ;
2014-05-17 23:15:06 +02:00
setTimeout ( function ( ) { textsecure . subscribeToPush ( message _callback ) ; } , 60000 ) ;
2014-05-17 07:53:58 +02:00
} ;
socket . onclose = function ( socketEvent ) {
console . log ( 'Server closed :(' ) ;
clearInterval ( pingInterval ) ;
subscribeToPushMessageSemaphore -- ;
2014-05-17 23:15:06 +02:00
setTimeout ( function ( ) { textsecure . subscribeToPush ( message _callback ) ; } , 60000 ) ;
2014-05-17 07:53:58 +02:00
} ;
socket . onopen = function ( socketEvent ) {
console . log ( 'Connected to server!' ) ;
2014-07-21 04:55:07 +02:00
pingInterval = setInterval ( function ( ) {
console . log ( "Sending server ping message." ) ;
if ( socket . readyState == socket . CLOSED || socket . readyState == socket . CLOSING ) {
socket . close ( ) ;
socket . onclose ( ) ;
} else
socket . send ( JSON . stringify ( { type : 2 } ) ) ;
} , 30000 ) ;
2014-05-17 07:53:58 +02:00
} ;
socket . onmessage = function ( response ) {
try {
var message = JSON . parse ( response . data ) ;
} catch ( e ) {
console . log ( 'Error parsing server JSON message: ' + response . responseBody . split ( "|" ) [ 1 ] ) ;
return ;
}
if ( message . type == 3 ) {
console . log ( "Got pong message" ) ;
} else if ( message . type === undefined && message . id !== undefined ) {
textsecure . crypto . decryptWebsocketMessage ( message . message ) . then ( function ( plaintext ) {
2014-05-21 21:04:05 +02:00
var proto = textsecure . protos . decodeIncomingPushMessageProtobuf ( getString ( plaintext ) ) ;
2014-05-17 07:53:58 +02:00
// After this point, a) decoding errors are not the server's fault, and
// b) we should handle them gracefully and tell the user they received an invalid message
console . log ( "Successfully decoded message with id: " + message . id ) ;
socket . send ( JSON . stringify ( { type : 1 , id : message . id } ) ) ;
return textsecure . crypto . handleIncomingPushMessageProto ( proto ) . then ( function ( decrypted ) {
2014-06-06 01:20:09 +02:00
// Now that its decrypted, validate the message and clean it up for consumer processing
// Note that messages may (generally) only perform one action and we ignore remaining fields
// after the first action.
if ( ( decrypted . flags & textsecure . protos . PushMessageContentProtobuf . Flags . END _SESSION )
== textsecure . protos . PushMessageContentProtobuf . Flags . END _SESSION )
return ;
if ( decrypted . flags != 0 )
throw new Error ( "Unknown flags in message" ) ;
2014-05-17 07:53:58 +02:00
var handleAttachment = function ( attachment ) {
return textsecure . api . getAttachment ( attachment . id ) . then ( function ( encryptedBin ) {
return textsecure . crypto . decryptAttachment ( encryptedBin , toArrayBuffer ( attachment . key ) ) . then ( function ( decryptedBin ) {
attachment . decrypted = decryptedBin ;
} ) ;
2014-05-15 06:26:37 +02:00
} ) ;
2014-05-17 07:53:58 +02:00
} ;
var promises = [ ] ;
2014-06-04 04:18:14 +02:00
if ( decrypted . group !== null ) {
var existingGroup = textsecure . storage . groups . getNumbers ( decrypted . group . id ) ;
if ( existingGroup === undefined ) {
if ( decrypted . group . type != textsecure . protos . PushMessageContentProtobuf . GroupContext . UPDATE )
throw new Error ( "Got message for unknown group" ) ;
textsecure . storage . groups . createNewGroup ( decrypted . group . members , decrypted . group . id ) ;
} else {
2014-06-06 01:20:09 +02:00
var fromIndex = existingGroup . indexOf ( proto . source ) ;
2014-06-04 04:18:14 +02:00
2014-06-06 01:20:09 +02:00
if ( fromIndex < 0 ) //TODO: This could be indication of a race...
2014-06-04 04:18:14 +02:00
throw new Error ( "Sender was not a member of the group they were sending from" ) ;
switch ( decrypted . group . type ) {
case textsecure . protos . PushMessageContentProtobuf . GroupContext . UPDATE :
if ( decrypted . group . avatar !== null )
promises . push ( handleAttachment ( decrypted . group . avatar ) ) ;
if ( existingGroup . filter ( function ( number ) { decrypted . group . members . indexOf ( number ) < 0 } ) . length != 0 )
throw new Error ( "Attempted to remove numbers from group with an UPDATE" ) ;
decrypted . group . added = decrypted . group . members . filter ( function ( number ) { return existingGroup . indexOf ( number ) < 0 ; } ) ;
var newGroup = textsecure . storage . groups . addNumbers ( decrypted . group . id , decrypted . group . added ) ;
if ( newGroup . length != decrypted . group . members . length ||
newGroup . filter ( function ( number ) { return decrypted . group . members . indexOf ( number ) < 0 ; } ) . length != 0 )
throw new Error ( "Error calculating group member difference" ) ;
2014-06-06 01:20:09 +02:00
2014-06-06 03:05:42 +02:00
//TODO: Also follow this path if avatar + name haven't changed (ie we should start storing those)
if ( decrypted . group . avatar === null && decrypted . group . added . length == 0 && decrypted . group . name === null )
return ;
//TODO: Strictly verify all numbers (ie dont let verifyNumber do any user-magic tweaking)
2014-06-06 01:20:09 +02:00
decrypted . body = null ;
decrypted . attachments = [ ] ;
2014-06-04 04:18:14 +02:00
break ;
case textsecure . protos . PushMessageContentProtobuf . GroupContext . QUIT :
textsecure . storage . groups . removeNumber ( decrypted . group . id , proto . source ) ;
2014-06-06 01:20:09 +02:00
decrypted . body = null ;
decrypted . attachments = [ ] ;
case textsecure . protos . PushMessageContentProtobuf . GroupContext . DELIVER :
decrypted . group . name = null ;
decrypted . group . members = [ ] ;
decrypted . group . avatar = null ;
2014-06-04 04:18:14 +02:00
break ;
default :
throw new Error ( "Unknown group message type" ) ;
}
}
}
for ( i in decrypted . attachments )
promises . push ( handleAttachment ( decrypted . attachments [ i ] ) ) ;
2014-05-19 09:06:28 +02:00
return Promise . all ( promises ) . then ( function ( ) {
2014-05-26 00:16:09 +02:00
message _callback ( { pushMessage : proto , message : decrypted } ) ;
2014-05-19 09:06:28 +02:00
} ) ;
2014-05-17 07:53:58 +02:00
} )
} ) . catch ( function ( e ) {
2014-05-27 21:29:44 +02:00
// TODO: Show "Invalid message" messages?
2014-05-17 07:53:58 +02:00
console . log ( "Error handling incoming message: " ) ;
console . log ( e ) ;
} ) ;
}
} ;
}
} ( ) ;
2014-05-27 20:40:39 +02:00
window . textsecure . register = function ( ) {
return function ( number , verificationCode , singleDevice , stepDone ) {
var signalingKey = textsecure . crypto . getRandomBytes ( 32 + 20 ) ;
textsecure . storage . putEncrypted ( 'signaling_key' , signalingKey ) ;
var password = btoa ( getString ( textsecure . crypto . getRandomBytes ( 16 ) ) ) ;
password = password . substring ( 0 , password . length - 2 ) ;
textsecure . storage . putEncrypted ( "password" , password ) ;
var registrationId = new Uint16Array ( textsecure . crypto . getRandomBytes ( 2 ) ) [ 0 ] ;
registrationId = registrationId & 0x3fff ;
textsecure . storage . putUnencrypted ( "registrationId" , registrationId ) ;
return textsecure . api . confirmCode ( number , verificationCode , password , signalingKey , registrationId , singleDevice ) . then ( function ( response ) {
if ( singleDevice )
response = 1 ;
var numberId = number + "." + response ;
textsecure . storage . putUnencrypted ( "number_id" , numberId ) ;
2014-06-30 00:15:27 +02:00
textsecure . storage . putUnencrypted ( "regionCode" , textsecure . utils . getRegionCodeForNumber ( number ) ) ;
2014-05-27 20:40:39 +02:00
stepDone ( 1 ) ;
if ( ! singleDevice ) {
//TODO: Do things???
stepDone ( 2 ) ;
}
return textsecure . crypto . generateKeys ( ) . then ( function ( keys ) {
stepDone ( 3 ) ;
return textsecure . api . registerKeys ( keys ) . then ( function ( ) {
stepDone ( 4 ) ;
} ) ;
} ) ;
} ) ;
}
} ( ) ;