Finish support for E2E encryption

This commit is contained in:
Marius Lindvall 2019-11-28 00:12:39 +01:00
parent a56457af90
commit 975ec8c2f9
15 changed files with 353 additions and 102 deletions

View file

@ -90,11 +90,13 @@ public enum Constants {
public static final String PACKET_PARAM_E2E_FLAG = "e2e";
public static final String PACKET_PARAM_GROUP_PIN = "pin";
public static final String PACKET_PARAM_ID_TO_ADOPT = "aid";
public static final String PACKET_PARAM_INIT_VECTOR = "iv";
public static final String PACKET_PARAM_INTERVAL = "int";
public static final String PACKET_PARAM_LATITUDE = "lat";
public static final String PACKET_PARAM_LONGITUDE = "lon";
public static final String PACKET_PARAM_NICKNAME = "nic";
public static final String PACKET_PARAM_PASSWORD = "pwd";
public static final String PACKET_PARAM_SALT = "salt";
public static final String PACKET_PARAM_SESSION_ID = "sid";
public static final String PACKET_PARAM_SHARE_ID = "lid";
public static final String PACKET_PARAM_SHARE_MODE = "mod";

View file

@ -5,12 +5,9 @@ import android.location.Location;
import android.util.Base64;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import info.varden.hauk.Constants;
import info.varden.hauk.R;
@ -50,7 +47,7 @@ public abstract class LocationUpdatePacket extends Packet {
super(ctx, session.getServerURL(), Constants.URL_PATH_POST_LOCATION);
setParameter(Constants.PACKET_PARAM_SESSION_ID, session.getID());
if (session.getE2EPassword() == null) {
if (session.getDerivableE2EKey() == null) {
// If not using end-to-end encryption, send parameters in plain text.
setParameter(Constants.PACKET_PARAM_LATITUDE, String.valueOf(location.getLatitude()));
setParameter(Constants.PACKET_PARAM_LONGITUDE, String.valueOf(location.getLongitude()));
@ -63,8 +60,9 @@ public abstract class LocationUpdatePacket extends Packet {
// We're using end-to-end encryption - generate an IV and encrypt all parameters.
try {
Cipher cipher = Cipher.getInstance(Constants.E2E_TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, session.getKeySpec(), new SecureRandom());
cipher.init(Cipher.ENCRYPT_MODE, session.getDerivableE2EKey().deriveSpec(), new SecureRandom());
byte[] iv = cipher.getIV();
setParameter(Constants.PACKET_PARAM_INIT_VECTOR, Base64.encodeToString(iv, Base64.DEFAULT));
setParameter(Constants.PACKET_PARAM_LATITUDE, Base64.encodeToString(cipher.doFinal(String.valueOf(location.getLatitude()).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT));
setParameter(Constants.PACKET_PARAM_LONGITUDE, Base64.encodeToString(cipher.doFinal(String.valueOf(location.getLongitude()).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT));

View file

@ -1,12 +1,16 @@
package info.varden.hauk.http;
import android.content.Context;
import android.util.Base64;
import androidx.annotation.Nullable;
import java.security.SecureRandom;
import info.varden.hauk.Constants;
import info.varden.hauk.R;
import info.varden.hauk.struct.AdoptabilityPreference;
import info.varden.hauk.struct.KeyDerivable;
import info.varden.hauk.struct.Session;
import info.varden.hauk.struct.Share;
import info.varden.hauk.struct.ShareMode;
@ -28,6 +32,11 @@ public class SessionInitiationPacket extends Packet {
*/
private ShareMode mode;
/**
* A salt used if the session is end-to-end encrypted.
*/
private final byte[] salt;
private SessionInitiationPacket(Context ctx, InitParameters params, ResponseHandler handler) {
super(ctx, params.getServerURL(), Constants.URL_PATH_CREATE_SHARE);
this.params = params;
@ -38,6 +47,16 @@ public class SessionInitiationPacket extends Packet {
if (params.getCustomID() != null) {
setParameter(Constants.PACKET_PARAM_SHARE_ID, params.getCustomID());
}
// Generate a random salt key derivation if using end-to-end encryption.
if (params.getE2EPassword() != null) {
SecureRandom rand = new SecureRandom();
this.salt = new byte[Constants.E2E_AES_KEY_SIZE / 8];
rand.nextBytes(this.salt);
// The backend needs to know about the salt so the frontend can derive the key using it.
setParameter(Constants.PACKET_PARAM_SALT, Base64.encodeToString(this.salt, Base64.DEFAULT));
} else {
this.salt = null;
}
setParameter(Constants.PACKET_PARAM_PASSWORD, params.getPassword());
setParameter(Constants.PACKET_PARAM_DURATION, String.valueOf(params.getDuration()));
setParameter(Constants.PACKET_PARAM_INTERVAL, String.valueOf(params.getInterval()));
@ -101,11 +120,11 @@ public class SessionInitiationPacket extends Packet {
}
// Check if the server is out of date for end-to-end encryption, if applicable.
String realE2EPassword = this.params.getE2EPassword();
if (realE2EPassword != null) {
if (!backendVersion.isAtLeast(Constants.VERSION_COMPAT_E2E_ENCRYPTION)) {
// If the server is out of date, disable E2E and warn the user.
realE2EPassword = null;
KeyDerivable e2eParams = null;
if (this.params.getE2EPassword() != null) {
if (backendVersion.isAtLeast(Constants.VERSION_COMPAT_E2E_ENCRYPTION)) {
e2eParams = new KeyDerivable(this.params.getE2EPassword(), this.salt);
} else {
this.handler.onE2EUnavailable(backendVersion);
}
}
@ -136,7 +155,7 @@ public class SessionInitiationPacket extends Packet {
}
// Create a share and pass it upstream.
Session session = new Session(this.params.getServerURL(), backendVersion, sessionID, this.params.getDuration() * TimeUtils.MILLIS_PER_SECOND + System.currentTimeMillis(), this.params.getInterval(), realE2EPassword);
Session session = new Session(this.params.getServerURL(), backendVersion, sessionID, this.params.getDuration() * TimeUtils.MILLIS_PER_SECOND + System.currentTimeMillis(), this.params.getInterval(), e2eParams);
Share share = new Share(session, viewURL, viewID, joinCode, this.mode);
this.handler.onSessionInitiated(share);

View file

@ -0,0 +1,69 @@
package info.varden.hauk.struct;
import java.io.Serializable;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import info.varden.hauk.Constants;
import info.varden.hauk.utils.StringUtils;
/**
* Serializable key spec that stores a password and salt for deriving a secret AES key spec.
*
* @author Marius Lindvall
*/
public final class KeyDerivable implements Serializable {
private static final long serialVersionUID = -4298542521894801298L;
/**
* Salt used in PBKDF2 for key derivation.
*/
private final byte[] salt;
/**
* End-to-end password to encrypt outgoing data with.
*/
private final String password;
/**
* Secret key spec cache to improve performance for key derivation.
*/
@SuppressWarnings("FieldNotUsedInToString")
private transient SecretKeySpec keySpec = null;
public KeyDerivable(String password, byte[] salt) {
this.password = password;
this.salt = salt.clone();
}
/**
* Derives a key spec from this derivable key.
*
* @return A secret key spec for use with encryption functions.
* @throws InvalidKeySpecException if the key spec doesn't exist.
* @throws NoSuchAlgorithmException if the algorithm doesn't exist.
*/
public SecretKeySpec deriveSpec() throws InvalidKeySpecException, NoSuchAlgorithmException {
if (this.keySpec == null) {
// E2E encryption is used, but the key spec hasn't been cached yet. Generate and cache
// it, then return the spec.
KeySpec ks = new PBEKeySpec(this.password.toCharArray(), this.salt, Constants.E2E_PBKDF2_ITERATIONS, Constants.E2E_AES_KEY_SIZE);
SecretKeyFactory kf = SecretKeyFactory.getInstance(Constants.E2E_KD_FUNCTION);
byte[] key = kf.generateSecret(ks).getEncoded();
this.keySpec = new SecretKeySpec(key, Constants.E2E_KEY_SPEC);
}
return this.keySpec;
}
@Override
public String toString() {
return "KeyDerivable{password=" + this.password
+ ",salt=0x" + StringUtils.bytesToHex(this.salt)
+ "}";
}
}

View file

@ -3,18 +3,10 @@ package info.varden.hauk.struct;
import androidx.annotation.Nullable;
import java.io.Serializable;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import info.varden.hauk.Constants;
import info.varden.hauk.utils.TimeUtils;
@ -54,34 +46,18 @@ public final class Session implements Serializable {
private final int interval;
/**
* End-to-end password to encrypt outgoing data with.
* End-to-end encryption parameters.
*/
@Nullable
private final String e2ePass;
private final KeyDerivable e2eParams;
/**
* Salt used in PBKDF2 for key derivation.
*/
@Nullable
private final byte[] salt;
/**
* Secret key spec cache to improve performance regarding key derivation.
*/
@Nullable
private transient SecretKeySpec keySpec = null;
public Session(String serverURL, Version backendVersion, String sessionID, long expiry, int interval, @Nullable String e2ePass) {
public Session(String serverURL, Version backendVersion, String sessionID, long expiry, int interval, @Nullable KeyDerivable e2eParams) {
this.serverURL = serverURL;
this.backendVersion = backendVersion;
this.sessionID = sessionID;
this.expiry = expiry;
this.interval = interval;
this.e2ePass = e2ePass;
SecureRandom rand = new SecureRandom();
this.salt = new byte[Constants.E2E_AES_KEY_SIZE / 8];
rand.nextBytes(this.salt);
this.e2eParams = e2eParams;
}
@Override
@ -91,7 +67,7 @@ public final class Session implements Serializable {
+ ",sessionID=" + this.sessionID
+ ",expiry=" + this.expiry
+ ",interval=" + this.interval
+ ",e2ePass=" + this.e2ePass
+ ",e2eParams=" + this.e2eParams
+ "}";
}
@ -165,24 +141,7 @@ public final class Session implements Serializable {
}
@Nullable
public String getE2EPassword() {
return this.e2ePass;
}
@Nullable
public SecretKeySpec getKeySpec() throws NoSuchAlgorithmException, InvalidKeySpecException {
if (this.e2ePass == null) {
// If end-to-end encryption is not used:
return null;
} else if (this.keySpec == null) {
// E2E encryption is used, but the keyspec hasn't been cached yet. Generate and cache
// it, then return the spec.
KeySpec ks = new PBEKeySpec(this.e2ePass.toCharArray(), this.salt, Constants.E2E_PBKDF2_ITERATIONS, Constants.E2E_AES_KEY_SIZE);
SecretKeyFactory kf = SecretKeyFactory.getInstance(Constants.E2E_KD_FUNCTION);
byte[] key = kf.generateSecret(ks).getEncoded();
this.keySpec = new SecretKeySpec(key, Constants.E2E_KEY_SPEC);
}
return this.keySpec;
public KeyDerivable getDerivableE2EKey() {
return this.e2eParams;
}
}

View file

@ -15,7 +15,6 @@ import java.io.Serializable;
* Helper class that serializes a serializable class to and from Base64-encoded strings for storage
* in Android shared preferences.
*/
@SuppressWarnings("StaticMethodOnlyUsedInOneClass")
public enum StringSerializer {
;

View file

@ -0,0 +1,28 @@
package info.varden.hauk.utils;
/**
* Utility class to process strings.
*
* @author Marius Lindvall
*/
public enum StringUtils {
;
// Byte array to hex string function by maybeWeCouldStealAVan
// https://stackoverflow.com/a/9855338
@SuppressWarnings("HardCodedStringLiteral")
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
@SuppressWarnings("MagicNumber")
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
}

View file

@ -27,6 +27,7 @@ $share = Share::fromShareID($memcache, $shid);
if (!$share->exists()) die($LANG['share_not_found']."\n");
if (!$share->getType() === SHARE_TYPE_ALONE) die($LANG['group_share_not_adoptable']."\n");
if (!$share->isAdoptable()) die($LANG['share_adoption_not_allowed']."\n");
if ($share->getHost()->isEncrypted()) die($LANG['e2e_adoption_not_allowed']."\n");
// Retrieve the target share.
$pin = $_POST["pin"];

View file

@ -26,15 +26,20 @@ if ($i < getConfig("min_interval")) die($LANG['interval_too_short']."\n");
// enabled by default in the app, but users are free to change this.
$adoptable = isset($_POST["ado"]) ? intval($_POST["ado"]) : 0;
$expire = time() + $d;
$encrypted = isset($_POST["e2e"]) ? intval($_POST["e2e"]) : 0;
// Require additional arguments depending on the given sharing mode.
switch ($mod) {
case SHARE_MODE_CREATE_GROUP:
// End-to-end encryption is not supported for group shares.
if ($encrypted > 0) die($LANG['group_e2e_unsupported']."\n");
requirePOST(
"nic" // Nickname to join the group share with.
);
break;
case SHARE_MODE_JOIN_GROUP:
// End-to-end encryption is not supported for group shares.
if ($encrypted > 0) die($LANG['group_e2e_unsupported']."\n");
requirePOST(
"nic", // Nickname to join the group share with.
"pin" // Group PIN for the group to attach to.
@ -42,6 +47,13 @@ switch ($mod) {
break;
}
// Also require salt when creating encrypted shares.
if ($encrypted) {
requirePOST(
"salt" // Salt used to derive the key in PBKDF2.
);
}
// If a custom link is requested, validate the ID.
$custom = filter_input(INPUT_POST, "lid", FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => "/^[\w-]+$/")));
if ($custom === false) $custom = null;
@ -70,7 +82,10 @@ switch ($mod) {
->save();
// Tell the session that it is posting to this share.
$host->addTarget($share)->save();
$host
->addTarget($share)
->setEncrypted($encrypted, $_POST["salt"])
->save();
$output = array(
"OK",

View file

@ -29,7 +29,9 @@ if (!$share->exists()) {
"type" => $share->getType(),
"expire" => $share->getExpirationTime(),
"interval" => $session->getInterval(),
"points" => $session->getPoints()
"points" => $session->getPoints(),
"encrypted" => $session->isEncrypted(),
"salt" => $session->getEncryptionSalt()
));
break;

View file

@ -14,16 +14,6 @@ requirePOST(
"sid" // Session ID to post to.
);
// Perform input validation.
$lat = floatval($_POST["lat"]);
$lon = floatval($_POST["lon"]);
$time = floatval($_POST["time"]);
if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) die($LANG['location_invalid']."\n");
// Not all devices report speed and accuracy, but if available, report them too.
$speed = isset($_POST["spd"]) ? floatval($_POST["spd"]) : null;
$accuracy = isset($_POST["acc"]) ? floatval($_POST["acc"]) : null;
$memcache = memConnect();
// Retrieve the session data from memcached.
@ -31,10 +21,40 @@ $sid = $_POST["sid"];
$session = new Client($memcache, $sid);
if (!$session->exists()) die($LANG['session_expired']."\n");
// The location data object contains the sharing interval (i), duration (d) and
// a location list (l). Each entry in the location list contains a latitude,
// longitude, timestamp, accuracy and speed, in that order, as an array.
$session->addPoint([$lat, $lon, $time, $accuracy, $speed])->save();
if (!$session->isEncrypted()) {
// Perform input validation.
$lat = floatval($_POST["lat"]);
$lon = floatval($_POST["lon"]);
$time = floatval($_POST["time"]);
if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) die($LANG['location_invalid']."\n");
// Not all devices report speed and accuracy, but if available, report them
// too.
$speed = isset($_POST["spd"]) ? floatval($_POST["spd"]) : null;
$accuracy = isset($_POST["acc"]) ? floatval($_POST["acc"]) : null;
// The location data object contains the sharing interval (i), duration (d)
// and a location list (l). Each entry in the location list contains a
// latitude, longitude, timestamp, accuracy and speed, in that order, as an
// array.
$session->addPoint([$lat, $lon, $time, $accuracy, $speed])->save();
} else {
// Input validation cannot be performed for end-to-end encrypted data.
$lat = $_POST["lat"];
$lon = $_POST["lon"];
$time = $_POST["time"];
$speed = isset($_POST["spd"]) ? $_POST["spd"] : null;
$accuracy = isset($_POST["acc"]) ? $_POST["acc"] : null;
// End-to-end encrypted connections also have an IV field used to decrypt
// the data fields.
requirePOST("iv");
$iv = $_POST["iv"];
// The IV field is prepended to the array to send to the client.
$session->addPoint([$iv, $lat, $lon, $time, $accuracy, $speed])->save();
}
if ($session->hasExpired()) {
echo $LANG['session_expired']."\n";

View file

@ -509,7 +509,9 @@ class Client {
"expire" => 0,
"interval" => null,
"targets" => array(),
"points" => array()
"points" => array(),
"encrypted" => 0,
"salt" => null
);
// Generate new session IDs for new sessions.
$this->sessionID = $this->generateSessionID();
@ -598,6 +600,23 @@ class Client {
return $this->sessionData["interval"];
}
// Sets whether or not this share is end-to-end encrypted.
public function setEncrypted($encrypted, $salt) {
$this->sessionData["encrypted"] = $encrypted;
$this->sessionData["salt"] = $salt;
return $this;
}
// Returns whether or not this share is end-to-end encrypted.
public function isEncrypted() {
return $this->sessionData["encrypted"] > 0;
}
// Returns the salt used to derive the key for end-to-end encryption.
public function getEncryptionSalt() {
return $this->sessionData["salt"];
}
// Adds a new share that this session is contributing location data to. Used
// so that shares can be cleaned up when end() is called. $share must be a
// Share instance. Does not take effect until save() is called.

View file

@ -16,3 +16,5 @@ $LANG['share_mode_unsupported'] = 'Unsupported share mode!';
$LANG['group_pin_invalid'] = 'Invalid group PIN!';
$LANG['session_invalid'] = 'Invalid session!';
$LANG['location_invalid'] = 'Invalid location!';
$LANG['group_e2e_unsupported'] = 'End-to-end encryption cannot be used for group shares!';
$LANG['e2e_adoption_not_allowed'] = 'This share is using end-to-end encryption and cannot be adopted!';

View file

@ -2,6 +2,10 @@
"page_title": "Hauk",
"expired_head": "Location expired",
"expired_body": "The shared location you tried to access was not found on the server. If this link worked before, the share might have expired.",
"e2e_password_prompt": "This share is protected by end-to-end encryption. Please enter the encryption password to access the share:",
"e2e_incorrect": "The encryption password you entered was wrong. Please try again:",
"e2e_unavailable_secure": "This share is protected by end-to-end encryption. Decryption is currently unavailable because you are not using HTTPS. Please ensure you are using HTTPS, then try again.",
"e2e_unsupported": "This share is protected by end-to-end encryption. Your browser does not appear to support the cryptographic functions required to decrypt such shares. Please try again with another web browser.",
"gnss_signal_head": "Please wait",
"gnss_signal_body": "Sender is waiting for GPS signal",
"point_app_to": "Point the Hauk app to this server to share your location:",

View file

@ -36,20 +36,22 @@ xhr.onreadystatechange = function() {
LANG[key] = data[key];
}
localizeHTML();
init();
}
};
xhr2.open('GET', './assets/lang/' + prefLang + '.json', true);
xhr2.send();
} else {
localizeHTML();
init();
}
}
};
xhr.open('GET', './assets/lang/en.json', true);
xhr.send();
// Put localized strings in HTML.
function localizeHTML() {
// Put localized strings in HTML.
var tags = document.querySelectorAll('[data-i18n]');
for (var key in tags) {
if (!tags.hasOwnProperty(key)) continue;
@ -62,6 +64,31 @@ function localizeHTML() {
}
}
// Starts fetching location data from the server. This is called after I18N has
// loaded to ensure strings are present for prompts.
function init() {
if (location.href.indexOf("?") === -1 || id == "") {
// If there is no share ID, show the root page.
var url = location.href.indexOf("?") === -1 ? location.href : location.href.substring(0, location.href.indexOf("?"));
var urlE = document.getElementById("url");
var indexE = document.getElementById("index");
if (urlE !== null) urlE.textContent = url;
if (indexE !== null) indexE.style.display = "block";
} else {
// Attempt to fetch location data from the server once.
getJSON("./api/fetch.php?id=" + id, function(data) {
// Initialize the Leaflet map.
initMap();
noGPS.style.display = "block";
processUpdate(data, true);
}, function() {
var notFoundE = document.getElementById("notfound");
if (notFoundE !== null) notFoundE.style.display = "block";
});
}
}
// For locating the viewer of the map. Watcher is a watchPosition() reference.
var watcher = null;
@ -284,7 +311,7 @@ function setNewInterval(expire, interval) {
clearInterval(countIntv);
setNewInterval(data.expire, data.interval);
}
processUpdate(data);
processUpdate(data, false);
}, function() {
// On failure to get new location data:
clearInterval(fetchIntv);
@ -296,28 +323,6 @@ function setNewInterval(expire, interval) {
}
var noGPS = document.getElementById("searching");
console.log(id);
if (location.href.indexOf("?") === -1 || id == "") {
// If there is no share ID, show the root page.
var url = location.href.indexOf("?") === -1 ? location.href : location.href.substring(0, location.href.indexOf("?"));
var urlE = document.getElementById("url");
var indexE = document.getElementById("index");
if (urlE !== null) urlE.textContent = url;
if (indexE !== null) indexE.style.display = "block";
} else {
// Attempt to fetch location data from the server once.
getJSON("./api/fetch.php?id=" + id, function(data) {
// Initialize the Leaflet map.
initMap();
noGPS.style.display = "block";
setNewInterval(data.expire, data.interval);
processUpdate(data);
}, function() {
var notFoundE = document.getElementById("notfound");
if (notFoundE !== null) notFoundE.style.display = "block";
});
}
// Whether or not an initial location has been received.
var hasReceivedFirst = false;
@ -328,8 +333,27 @@ var hasInitiated = false;
// The user being followed on the map.
var following = null;
// The decryption key for end-to-end encrypted shares.
var aesKey = null;
// Whether or not the user has already entered an incorrect encryption password
// at least once.
var hasEnteredPass = false;
// Converts a base64-encoded string to a Uint8Array ArrayBuffer for use with
// WebCrypto.
function byteArray(base64) {
var raw = atob(base64);
var len = raw.length;
var arr = new Uint8Array(new ArrayBuffer(len));
for (var i = 0; i < len; i++) {
arr[i] = raw.charCodeAt(i);
}
return arr;
}
// Parses the data returned from ./api/fetch.php and updates the map marker.
function processUpdate(data) {
function processUpdate(data, init) {
var users = {};
var multiUser = false;
if (data.type == SHARE_TYPE_ALONE) {
@ -340,6 +364,96 @@ function processUpdate(data) {
multiUser = true;
}
// Check for crypto support if necessary.
if (data.encrypted && !("crypto" in window)) {
alert(LANG["e2e_unsupported"]);
return;
} else if (data.encrypted && !("subtle" in window.crypto)) {
if (!window.isSecureContext) {
alert(LANG["e2e_unavailable_secure"]);
} else {
alert(LANG["e2e_unsupported"]);
}
return;
}
if (data.encrypted && aesKey == null) {
// If using end-to-end encryption, we need to decrypt the data. We have
// not obtained an AES key yet, so prompt the user for it.
var password = prompt(hasEnteredPass ? LANG["e2e_incorrect"] : LANG["e2e_password_prompt"]);
if (password == null) return;
hasEnteredPass = true;
// Get the salt in binary format.
var salt = byteArray(data.salt);
// Derive the encryption key using PBKDF2 with SHA-1. SHA-1 was chosen
// because of availability in Android.
crypto.subtle
.importKey("raw", new TextEncoder("utf-8").encode(password), "PBKDF2", false, ["deriveKey"])
.then(key => crypto.subtle.deriveKey(
{name: "PBKDF2", salt: salt, iterations: 65536, hash: "SHA-1"},
key,
{name: "AES-CBC", length: 256},
false,
["decrypt"]
))
.then(key => {
// Store the crypto key and re-process the update.
aesKey = key;
processUpdate(data, init);
});
return;
} else if (data.encrypted) {
// The data is encrypted, but now we have a key we can use to decrypt
// it. Decrypt each point using the key.
var algo = {name: "AES-CBC"};
var pointPromises = [];
for (var i = 0; i < data.points.length; i++) {
algo.iv = byteArray(data.points[i][0]);
var promises = [];
for (var j = 1; j < data.points[i].length; j++) {
promises.push(crypto.subtle.decrypt(algo, aesKey, byteArray(data.points[i][j])));
}
pointPromises.push(Promise.all(promises));
}
// Wait for all points to be decrypted.
Promise
.all(pointPromises)
.then(function(values) {
// Parse all points and conver them to floating point values
// (all values in the array are currently numbers).
var decoder = new TextDecoder("utf-8");
for (var i = 0; i < values.length; i++) {
for (var j = 0; j < values[i].length; j++) {
data.points[i][j] = parseFloat(decoder.decode(values[i][j]));
}
// The IV was the first item in the array, so all items have
// been shifted up once. Pop the last array element off.
data.points[i].pop();
}
// Flag the data as unencrypted and re-process the update.
data.encrypted = false;
processUpdate(data, init);
})
.catch(function(error) {
// Decryption error. Most likely incorrect password. Reset the
// key and prompt the user for the password again.
aesKey = null;
processUpdate(data, init);
});
return;
}
// If flagged to initialize, set up polling.
if (init) setNewInterval(data.expire, data.interval);
for (var user in users) {
if (!users.hasOwnProperty(user)) continue;
var locData = users[user];