Hook up signal device linking view.

This commit is contained in:
Mikunj 2019-11-20 09:50:40 +11:00
parent 549631848d
commit 0f5db5aa33
13 changed files with 919 additions and 863 deletions

File diff suppressed because it is too large Load Diff

View File

@ -12,9 +12,11 @@
android:singleLine="true" android:singleLine="true"
android:ellipsize="marquee" android:ellipsize="marquee"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<!--
<TextView android:id="@+id/created" <TextView android:id="@+id/created"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -29,5 +31,5 @@
android:textColor="?attr/conversation_list_item_subject_color" android:textColor="?attr/conversation_list_item_subject_color"
android:fontFamily="sans-serif-light" android:fontFamily="sans-serif-light"
android:layout_marginBottom="8dp" /> android:layout_marginBottom="8dp" />
-->
</org.thoughtcrime.securesms.DeviceListItem> </org.thoughtcrime.securesms.DeviceListItem>

View File

@ -299,6 +299,7 @@
<string name="DeviceListActivity_unlinking_device">Unlinking device...</string> <string name="DeviceListActivity_unlinking_device">Unlinking device...</string>
<string name="DeviceListActivity_unlinking_device_no_ellipsis">Unlinking device</string> <string name="DeviceListActivity_unlinking_device_no_ellipsis">Unlinking device</string>
<string name="DeviceListActivity_network_failed">Network failed!</string> <string name="DeviceListActivity_network_failed">Network failed!</string>
<string name="DeviceListActivity_unlinked_device">Successfully unlinked device</string>
<!-- DeviceListItem --> <!-- DeviceListItem -->
<string name="DeviceListItem_unnamed_device">Unnamed device</string> <string name="DeviceListItem_unnamed_device">Unnamed device</string>
@ -1576,7 +1577,7 @@
<string name="activity_settings_public_key_copied_message">Copied to clipboard</string> <string name="activity_settings_public_key_copied_message">Copied to clipboard</string>
<string name="activity_settings_share_public_key_button_title">Share Public Key</string> <string name="activity_settings_share_public_key_button_title">Share Public Key</string>
<string name="activity_settings_show_qr_code_button_title">Show QR Code</string> <string name="activity_settings_show_qr_code_button_title">Show QR Code</string>
<string name="activity_settings_link_device_button_title">Link Device</string> <string name="activity_settings_linked_devices_button_title">Linked Device</string>
<string name="activity_settings_show_seed_button_title">Show Seed</string> <string name="activity_settings_show_seed_button_title">Show Seed</string>
<string name="activity_settings_seed_dialog_title">Your Seed</string> <string name="activity_settings_seed_dialog_title">Your Seed</string>
<string name="activity_settings_seed_dialog_copy_button_title">Copy</string> <string name="activity_settings_seed_dialog_copy_button_title">Copy</string>

View File

@ -41,8 +41,8 @@
android:title="@string/activity_settings_show_qr_code_button_title" android:title="@string/activity_settings_show_qr_code_button_title"
android:icon="@drawable/icon_qr_code"/> android:icon="@drawable/icon_qr_code"/>
<Preference android:key="preference_category_link_device" <Preference android:key="preference_category_linked_devices"
android:title="Link Device" android:title="@string/activity_settings_linked_devices_button_title"
android:icon="@drawable/icon_link"/> android:icon="@drawable/icon_link"/>
<Preference android:key="preference_category_seed" <Preference android:key="preference_category_seed"

View File

@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.loki.DeviceLinkingDialog; import org.thoughtcrime.securesms.loki.DeviceLinkingDialog;
import org.thoughtcrime.securesms.loki.DeviceLinkingDialogDelegate; import org.thoughtcrime.securesms.loki.DeviceLinkingDialogDelegate;
import org.thoughtcrime.securesms.loki.DeviceLinkingView; import org.thoughtcrime.securesms.loki.DeviceLinkingView;
import org.thoughtcrime.securesms.loki.LinkedDevicesActivity;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities; import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.loki.QRCodeDialog; import org.thoughtcrime.securesms.loki.QRCodeDialog;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
@ -89,7 +90,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
// private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced"; // private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
private static final String PREFERENCE_CATEGORY_PUBLIC_KEY = "preference_category_public_key"; private static final String PREFERENCE_CATEGORY_PUBLIC_KEY = "preference_category_public_key";
private static final String PREFERENCE_CATEGORY_QR_CODE = "preference_category_qr_code"; private static final String PREFERENCE_CATEGORY_QR_CODE = "preference_category_qr_code";
private static final String PREFERENCE_CATEGORY_LINK_DEVICE = "preference_category_link_device"; private static final String PREFERENCE_CATEGORY_LINKED_DEVICES = "preference_category_linked_devices";
private static final String PREFERENCE_CATEGORY_SEED = "preference_category_seed"; private static final String PREFERENCE_CATEGORY_SEED = "preference_category_seed";
private final DynamicTheme dynamicTheme = new DynamicTheme(); private final DynamicTheme dynamicTheme = new DynamicTheme();
@ -192,20 +193,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
this.findPreference(PREFERENCE_CATEGORY_QR_CODE) this.findPreference(PREFERENCE_CATEGORY_QR_CODE)
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_QR_CODE)); .setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_QR_CODE));
Preference linkDevicePreference = this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE); Preference linkDevicesPreference = this.findPreference(PREFERENCE_CATEGORY_LINKED_DEVICES);
linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_LINK_DEVICE)); linkDevicesPreference.setVisible(isMasterDevice);
linkDevicesPreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_LINKED_DEVICES));
// Disable if we hit the cap of 1 linked device
if (isMasterDevice) {
Context context = getContext();
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
boolean isDeviceLinkingEnabled = DatabaseFactory.getLokiAPIDatabase(context).getPairingAuthorisations(userHexEncodedPublicKey).size() <= 1;
linkDevicePreference.setEnabled(isDeviceLinkingEnabled);
linkDevicePreference.getIcon().setAlpha(isDeviceLinkingEnabled ? 255 : 124);
} else {
// Hide if this is a slave device
linkDevicePreference.setVisible(false);
}
Preference seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED); Preference seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED);
// Hide if this is a slave device // Hide if this is a slave device
@ -299,7 +289,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
// this.findPreference(PREFERENCE_CATEGORY_ADVANCED).setIcon(advanced); // this.findPreference(PREFERENCE_CATEGORY_ADVANCED).setIcon(advanced);
this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY).setIcon(publicKey); this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY).setIcon(publicKey);
this.findPreference(PREFERENCE_CATEGORY_QR_CODE).setIcon(qrCode); this.findPreference(PREFERENCE_CATEGORY_QR_CODE).setIcon(qrCode);
this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE).setIcon(linkDevice); this.findPreference(PREFERENCE_CATEGORY_LINKED_DEVICES).setIcon(linkDevice);
this.findPreference(PREFERENCE_CATEGORY_SEED).setIcon(seed); this.findPreference(PREFERENCE_CATEGORY_SEED).setIcon(seed);
} }
@ -360,7 +350,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
case PREFERENCE_CATEGORY_QR_CODE: case PREFERENCE_CATEGORY_QR_CODE:
QRCodeDialog.INSTANCE.show(getContext()); QRCodeDialog.INSTANCE.show(getContext());
break; break;
case PREFERENCE_CATEGORY_LINK_DEVICE: case PREFERENCE_CATEGORY_LINKED_DEVICES:
Intent intent = new Intent(getActivity(), LinkedDevicesActivity.class);
startActivity(intent);
DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, this); DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, this);
break; break;
case PREFERENCE_CATEGORY_SEED: case PREFERENCE_CATEGORY_SEED:

View File

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.ListFragment; import android.support.v4.app.ListFragment;
@ -17,25 +16,21 @@ import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button; import android.widget.Button;
import android.widget.ListView; import android.widget.ListView;
import android.widget.Toast;
import com.melnykov.fab.FloatingActionButton; import com.melnykov.fab.FloatingActionButton;
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader; import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.devicelist.Device; import org.thoughtcrime.securesms.devicelist.Device;
import org.thoughtcrime.securesms.jobs.RefreshUnidentifiedDeliveryAbilityJob; import org.thoughtcrime.securesms.loki.MnemonicUtilities;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import java.io.IOException; import java.io.File;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject; import org.whispersystems.libsignal.util.guava.Function;
import network.loki.messenger.R; import network.loki.messenger.R;
@ -46,14 +41,13 @@ public class DeviceListFragment extends ListFragment
private static final String TAG = DeviceListFragment.class.getSimpleName(); private static final String TAG = DeviceListFragment.class.getSimpleName();
@Inject private File languageFileDirectory;
SignalServiceAccountManager accountManager;
private Locale locale; private Locale locale;
private View empty; private View empty;
private View progressContainer; private View progressContainer;
private FloatingActionButton addDeviceButton; private FloatingActionButton addDeviceButton;
private Button.OnClickListener addDeviceButtonListener; private Button.OnClickListener addDeviceButtonListener;
private Function<String, Void> handleDisconnectDevice;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -82,6 +76,7 @@ public class DeviceListFragment extends ListFragment
@Override @Override
public void onActivityCreated(Bundle bundle) { public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle); super.onActivityCreated(bundle);
this.languageFileDirectory = MnemonicUtilities.getLanguageFileDirectory(getContext());
getLoaderManager().initLoader(0, null, this); getLoaderManager().initLoader(0, null, this);
getListView().setOnItemClickListener(this); getListView().setOnItemClickListener(this);
} }
@ -90,12 +85,20 @@ public class DeviceListFragment extends ListFragment
this.addDeviceButtonListener = listener; this.addDeviceButtonListener = listener;
} }
public void setHandleDisconnectDevice(Function<String, Void> handler) {
this.handleDisconnectDevice = handler;
}
public void setAddDeviceButtonVisible(boolean visible) {
addDeviceButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
}
@Override @Override
public @NonNull Loader<List<Device>> onCreateLoader(int id, Bundle args) { public @NonNull Loader<List<Device>> onCreateLoader(int id, Bundle args) {
empty.setVisibility(View.GONE); empty.setVisibility(View.GONE);
progressContainer.setVisibility(View.VISIBLE); progressContainer.setVisibility(View.VISIBLE);
return new DeviceListLoader(getActivity(), accountManager); return new DeviceListLoader(getActivity(), languageFileDirectory);
} }
@Override @Override
@ -125,7 +128,7 @@ public class DeviceListFragment extends ListFragment
@Override @Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final String deviceName = ((DeviceListItem)view).getDeviceName(); final String deviceName = ((DeviceListItem)view).getDeviceName();
final long deviceId = ((DeviceListItem)view).getDeviceId(); final String deviceId = ((DeviceListItem)view).getDeviceId();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName)); builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName));
@ -134,12 +137,16 @@ public class DeviceListFragment extends ListFragment
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
handleDisconnectDevice(deviceId); if (handleDisconnectDevice != null) { handleDisconnectDevice.apply(deviceId); }
} }
}); });
builder.show(); builder.show();
} }
public void refresh() {
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
}
private void handleLoaderFailed() { private void handleLoaderFailed() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage(R.string.DeviceListActivity_network_connection_failed); builder.setMessage(R.string.DeviceListActivity_network_connection_failed);
@ -167,34 +174,6 @@ public class DeviceListFragment extends ListFragment
builder.show(); builder.show();
} }
private void handleDisconnectDevice(final long deviceId) {
new ProgressDialogAsyncTask<Void, Void, Void>(getActivity(),
R.string.DeviceListActivity_unlinking_device_no_ellipsis,
R.string.DeviceListActivity_unlinking_device)
{
@Override
protected Void doInBackground(Void... params) {
try {
accountManager.removeDevice(deviceId);
ApplicationContext.getInstance(getContext())
.getJobManager()
.add(new RefreshUnidentifiedDeliveryAbilityJob());
} catch (IOException e) {
Log.w(TAG, e);
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
}
return null;
}
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (addDeviceButtonListener != null) addDeviceButtonListener.onClick(v); if (addDeviceButtonListener != null) addDeviceButtonListener.onClick(v);

View File

@ -15,7 +15,7 @@ import network.loki.messenger.R;
public class DeviceListItem extends LinearLayout { public class DeviceListItem extends LinearLayout {
private long deviceId; private String deviceId;
private TextView name; private TextView name;
private TextView created; private TextView created;
private TextView lastActive; private TextView lastActive;
@ -32,14 +32,15 @@ public class DeviceListItem extends LinearLayout {
public void onFinishInflate() { public void onFinishInflate() {
super.onFinishInflate(); super.onFinishInflate();
this.name = (TextView) findViewById(R.id.name); this.name = (TextView) findViewById(R.id.name);
this.created = (TextView) findViewById(R.id.created); // this.created = (TextView) findViewById(R.id.created);
this.lastActive = (TextView) findViewById(R.id.active); // this.lastActive = (TextView) findViewById(R.id.active);
} }
public void set(Device deviceInfo, Locale locale) { public void set(Device deviceInfo, Locale locale) {
if (TextUtils.isEmpty(deviceInfo.getName())) this.name.setText(R.string.DeviceListItem_unnamed_device); if (TextUtils.isEmpty(deviceInfo.getName())) this.name.setText(R.string.DeviceListItem_unnamed_device);
else this.name.setText(deviceInfo.getName()); else this.name.setText(deviceInfo.getName());
/*
this.created.setText(getContext().getString(R.string.DeviceListItem_linked_s, this.created.setText(getContext().getString(R.string.DeviceListItem_linked_s,
DateUtils.getDayPrecisionTimeSpanString(getContext(), DateUtils.getDayPrecisionTimeSpanString(getContext(),
locale, locale,
@ -49,11 +50,12 @@ public class DeviceListItem extends LinearLayout {
DateUtils.getDayPrecisionTimeSpanString(getContext(), DateUtils.getDayPrecisionTimeSpanString(getContext(),
locale, locale,
deviceInfo.getLastSeen()))); deviceInfo.getLastSeen())));
*/
this.deviceId = deviceInfo.getId(); this.deviceId = deviceInfo.getId();
} }
public long getDeviceId() { public String getDeviceId() {
return deviceId; return deviceId;
} }

View File

@ -7,10 +7,13 @@ import android.text.TextUtils;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.devicelist.Device; import org.thoughtcrime.securesms.devicelist.Device;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.MnemonicUtilities;
import org.thoughtcrime.securesms.util.AsyncLoader; import org.thoughtcrime.securesms.util.AsyncLoader;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPrivateKey; import org.whispersystems.libsignal.ecc.ECPrivateKey;
@ -19,7 +22,10 @@ import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.MessageDigest; import java.security.MessageDigest;
@ -33,93 +39,42 @@ import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import static org.thoughtcrime.securesms.devicelist.DeviceNameProtos.*; import static org.thoughtcrime.securesms.devicelist.DeviceNameProtos.*;
import static org.whispersystems.signalservice.loki.utilities.TrimmingKt.removing05PrefixIfNeeded;
public class DeviceListLoader extends AsyncLoader<List<Device>> { public class DeviceListLoader extends AsyncLoader<List<Device>> {
private static final String TAG = DeviceListLoader.class.getSimpleName(); private static final String TAG = DeviceListLoader.class.getSimpleName();
private MnemonicCodec mnemonicCodec;
private final SignalServiceAccountManager accountManager; public DeviceListLoader(Context context, File languageFileDirectory) {
public DeviceListLoader(Context context, SignalServiceAccountManager accountManager) {
super(context); super(context);
this.accountManager = accountManager; this.mnemonicCodec = new MnemonicCodec(languageFileDirectory);
} }
@Override @Override
public List<Device> loadInBackground() { public List<Device> loadInBackground() {
try { try {
List<Device> devices = Stream.of(accountManager.getDevices()) String ourPublicKey = TextSecurePreferences.getLocalNumber(getContext());
.filter(d -> d.getId() != SignalServiceAddress.DEFAULT_DEVICE_ID) List<String> secondaryDevicePublicKeys = LokiStorageAPI.shared.getSecondaryDevicePublicKeys(ourPublicKey).get();
.map(this::mapToDevice) List<Device> devices = Stream.of(secondaryDevicePublicKeys).map(this::mapToDevice).toList();
.toList();
Collections.sort(devices, new DeviceComparator()); Collections.sort(devices, new DeviceComparator());
return devices; return devices;
} catch (IOException e) { } catch (Exception e) {
Log.w(TAG, e); Log.w(TAG, e);
return null; return null;
} }
} }
private Device mapToDevice(@NonNull DeviceInfo deviceInfo) { private Device mapToDevice(@NonNull String hexEncodedPublicKey) {
try { long now = System.currentTimeMillis();
if (TextUtils.isEmpty(deviceInfo.getName()) || deviceInfo.getName().length() < 4) { return new Device(hexEncodedPublicKey, MnemonicUtilities.getFirst3Words(mnemonicCodec, hexEncodedPublicKey), now, now);
throw new IOException("Invalid DeviceInfo name.");
}
DeviceName deviceName = DeviceName.parseFrom(Base64.decode(deviceInfo.getName()));
if (!deviceName.hasCiphertext() || !deviceName.hasEphemeralPublic() || !deviceName.hasSyntheticIv()) {
throw new IOException("Got a DeviceName that wasn't properly populated.");
}
byte[] syntheticIv = deviceName.getSyntheticIv().toByteArray();
byte[] cipherText = deviceName.getCiphertext().toByteArray();
ECPrivateKey identityKey = IdentityKeyUtil.getIdentityKeyPair(getContext()).getPrivateKey();
ECPublicKey ephemeralPublic = Curve.decodePoint(deviceName.getEphemeralPublic().toByteArray(), 0);
byte[] masterSecret = Curve.calculateAgreement(ephemeralPublic, identityKey);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(masterSecret, "HmacSHA256"));
byte[] cipherKeyPart1 = mac.doFinal("cipher".getBytes());
mac.init(new SecretKeySpec(cipherKeyPart1, "HmacSHA256"));
byte[] cipherKey = mac.doFinal(syntheticIv);
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(new byte[16]));
final byte[] plaintext = cipher.doFinal(cipherText);
mac.init(new SecretKeySpec(masterSecret, "HmacSHA256"));
byte[] verificationPart1 = mac.doFinal("auth".getBytes());
mac.init(new SecretKeySpec(verificationPart1, "HmacSHA256"));
byte[] verificationPart2 = mac.doFinal(plaintext);
byte[] ourSyntheticIv = ByteUtil.trim(verificationPart2, 16);
if (!MessageDigest.isEqual(ourSyntheticIv, syntheticIv)) {
throw new GeneralSecurityException("The computed syntheticIv didn't match the actual syntheticIv.");
}
return new Device(deviceInfo.getId(), new String(plaintext), deviceInfo.getCreated(), deviceInfo.getLastSeen());
} catch (IOException e) {
Log.w(TAG, "Failed while reading the protobuf.", e);
} catch (GeneralSecurityException | InvalidKeyException e) {
Log.w(TAG, "Failed during decryption.", e);
}
return new Device(deviceInfo.getId(), deviceInfo.getName(), deviceInfo.getCreated(), deviceInfo.getLastSeen());
} }
private static class DeviceComparator implements Comparator<Device> { private static class DeviceComparator implements Comparator<Device> {
@Override @Override
public int compare(Device lhs, Device rhs) { public int compare(Device lhs, Device rhs) {
if (lhs.getCreated() < rhs.getCreated()) return -1; return lhs.getName().compareTo(rhs.getName());
else if (lhs.getCreated() != rhs.getCreated()) return 1;
else return 0;
} }
} }
} }

View File

@ -2,19 +2,19 @@ package org.thoughtcrime.securesms.devicelist;
public class Device { public class Device {
private final long id; private final String id;
private final String name; private final String name;
private final long created; private final long created;
private final long lastSeen; private final long lastSeen;
public Device(long id, String name, long created, long lastSeen) { public Device(String id, String name, long created, long lastSeen) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.created = created; this.created = created;
this.lastSeen = lastSeen; this.lastSeen = lastSeen;
} }
public long getId() { public String getId() {
return id; return id;
} }

View File

@ -32,31 +32,10 @@ class DeviceLinkingView private constructor(context: Context, attrs: AttributeSe
private constructor(context: Context) : this(context, null) private constructor(context: Context) : this(context, null)
init { init {
setUpLanguageFileDirectory() languageFileDirectory = MnemonicUtilities.getLanguageFileDirectory(context)
setUpViewHierarchy() setUpViewHierarchy()
} }
private fun setUpLanguageFileDirectory() {
val languages = listOf( "english", "japanese", "portuguese", "spanish" )
val directory = File(context.applicationInfo.dataDir)
for (language in languages) {
val fileName = "$language.txt"
if (directory.list().contains(fileName)) { continue }
val inputStream = context.assets.open("mnemonic/$fileName")
val file = File(directory, fileName)
val outputStream = FileOutputStream(file)
val buffer = ByteArray(1024)
while (true) {
val count = inputStream.read(buffer)
if (count < 0) { break }
outputStream.write(buffer, 0, count)
}
inputStream.close()
outputStream.close()
}
languageFileDirectory = directory
}
private fun setUpViewHierarchy() { private fun setUpViewHierarchy() {
inflate(context, R.layout.view_device_linking, this) inflate(context, R.layout.view_device_linking, this)
spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN) spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
@ -72,8 +51,8 @@ class DeviceLinkingView private constructor(context: Context, attrs: AttributeSe
explanationTextView.text = resources.getString(explanationID) explanationTextView.text = resources.getString(explanationID)
mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
if (mode == Mode.Slave) { if (mode == Mode.Slave) {
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded() val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ") mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), hexEncodedPublicKey)
} }
authorizeButton.visibility = View.GONE authorizeButton.visibility = View.GONE
authorizeButton.setOnClickListener { authorizePairing() } authorizeButton.setOnClickListener { authorizePairing() }

View File

@ -0,0 +1,76 @@
package org.thoughtcrime.securesms.loki
import android.os.Bundle
import android.view.MenuItem
import android.widget.Toast
import org.thoughtcrime.securesms.*
import org.thoughtcrime.securesms.util.DynamicTheme
import org.thoughtcrime.securesms.util.DynamicLanguage
import network.loki.messenger.R
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
class LinkedDevicesActivity : PassphraseRequiredActionBarActivity() {
companion object {
private val TAG = DeviceActivity::class.java.simpleName
}
private val dynamicTheme = DynamicTheme()
private val dynamicLanguage = DynamicLanguage()
private lateinit var deviceListFragment: DeviceListFragment
public override fun onPreCreate() {
dynamicTheme.onCreate(this)
dynamicLanguage.onCreate(this)
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.AndroidManifest__linked_devices)
this.deviceListFragment = DeviceListFragment()
this.deviceListFragment.setAddDeviceButtonListener {
// TODO: Hook up add device
}
this.deviceListFragment.setHandleDisconnectDevice { devicePublicKey ->
// Purge the device pairing from our database
val ourPublicKey = TextSecurePreferences.getLocalNumber(this)
val database = DatabaseFactory.getLokiAPIDatabase(this)
database.removePairingAuthorisation(ourPublicKey, devicePublicKey)
// Update mapping on the file server
LokiStorageAPI.shared.updateUserDeviceMappings()
// Send a background message to let the device know that it has been revoked
MessageSender.sendBackgroundMessage(this, devicePublicKey)
// Refresh the list
refresh()
Toast.makeText(this, R.string.DeviceListActivity_unlinked_device, Toast.LENGTH_LONG).show()
return@setHandleDisconnectDevice null
}
initFragment(android.R.id.content, deviceListFragment, dynamicLanguage.currentLocale)
refresh()
}
private fun refresh() {
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)
val isDeviceLinkingEnabled = DatabaseFactory.getLokiAPIDatabase(this).getPairingAuthorisations(userHexEncodedPublicKey).isEmpty()
this.deviceListFragment.setAddDeviceButtonVisible(isDeviceLinkingEnabled)
this.deviceListFragment.refresh()
}
public override fun onResume() {
super.onResume()
dynamicTheme.onResume(this)
dynamicLanguage.onResume(this)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
finish()
return true
}
return false
}
}

View File

@ -189,6 +189,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
database.delete(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey )) database.delete(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey ))
} }
fun removePairingAuthorisation(primaryDevicePublicKey: String, secondaryDevicePublicKey: String) {
val database = databaseHelper.readableDatabase
database.delete(pairingAuthorisationCache, "${Companion.primaryDevicePublicKey} = ? OR ${Companion.secondaryDevicePublicKey} = ?", arrayOf( primaryDevicePublicKey, secondaryDevicePublicKey ))
}
} }
// region Convenience // region Convenience

View File

@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.loki
import android.content.Context
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
import java.io.File
import java.io.FileOutputStream
object MnemonicUtilities {
@JvmStatic
public fun getLanguageFileDirectory(context: Context): File {
val languages = listOf( "english", "japanese", "portuguese", "spanish" )
val directory = File(context.applicationInfo.dataDir)
for (language in languages) {
val fileName = "$language.txt"
if (directory.list().contains(fileName)) { continue }
val inputStream = context.assets.open("mnemonic/$fileName")
val file = File(directory, fileName)
val outputStream = FileOutputStream(file)
val buffer = ByteArray(1024)
while (true) {
val count = inputStream.read(buffer)
if (count < 0) { break }
outputStream.write(buffer, 0, count)
}
inputStream.close()
outputStream.close()
}
return directory
}
@JvmStatic
public fun getFirst3Words(codec: MnemonicCodec, hexEncodedPublicKey: String): String {
return codec.encode(hexEncodedPublicKey.removing05PrefixIfNeeded()).split(" ").slice(0 until 3).joinToString(" ")
}
}