Skip to content

Commit

Permalink
WIP: Add support for identities
Browse files Browse the repository at this point in the history
This commit adds some of the first steps towards implementing
identities:

* A type system on top of UnknownMessage
* IdentityAnnouncementMessage for announcing identities
* libsignal compatibility stubs
* Initial onboarding UI to create an identity

A few things are known to be broken:

* libsignal integration is not fully implemented and has not been tested
* Onboarding UI does not close once the identity has been created
* Onboarding UI does not prevent creating a username that conflicts with
  an existing one
  • Loading branch information
aarmea committed Jun 22, 2018
1 parent 05b750c commit 56d072b
Show file tree
Hide file tree
Showing 28 changed files with 919 additions and 88 deletions.
2 changes: 1 addition & 1 deletion LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2016-2017 Noise developers
Copyright (c) 2016-2018 Albert Armea

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
Expand Down
31 changes: 23 additions & 8 deletions README.md
@@ -1,12 +1,10 @@
Noise
=====
# Noise

A chat app for the end of the world.

[![Build Status](https://travis-ci.org/aarmea/noise.svg?branch=master)](https://travis-ci.org/aarmea/noise)

What's wrong with chat today
----------------------------
## What's wrong with chat today

On traditional chat platforms, mobile devices connect over the Internet to a central server which is responsible for relaying messages to their destinations. This has a few problems:

Expand All @@ -21,8 +19,7 @@ On traditional chat platforms, mobile devices connect over the Internet to a cen
[censorship-nk]: https://en.wikipedia.org/wiki/Human_rights_in_North_Korea#Civil_liberties
[censorship-turkey]: https://www.afp.com/en/news/826/turkey-gives-watchdog-power-block-internet-broadcasts-doc-12z0r61

How Noise will fix this
-----------------------
## How Noise will fix this

A formal paper describing Noise could be titled: "Adapting epidemic routing for commodity phones in adversarial conditions". Let's unwrap this:

Expand All @@ -46,8 +43,26 @@ This provides a prioritized flat database of cleartext messages. Two-way chat ca

I also plan on making this database available as a local API to allow developers to build robust communications into their apps.

What works so far
-----------------
## What works so far

* Epidemic routing over Bluetooth
* Proof-of-work for message validation

## Legal

### Cryptography notice

This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software.
BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted.
See <http://www.wassenaar.org/> for more information.

The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms.
The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code.

### Experimental software

This project has not been reviewed by a security expert. If you entrust Noise with sensitive information, you are doing so at your own risk.

### License

You may use Noise under the terms of the MIT license. See LICENSE for more details.
8 changes: 6 additions & 2 deletions app/build.gradle
Expand Up @@ -41,6 +41,7 @@ android {
}

dependencies {
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
Expand All @@ -52,13 +53,16 @@ dependencies {

compile group: 'net.vidageek', name: 'mirror', version: '1.6.1'

compile 'com.squareup.okio:okio:1.14.0'
compile 'com.squareup.okio:okio:1.14.1'

compile "io.reactivex.rxjava2:rxandroid:2.0.2"
compile "io.reactivex.rxjava2:rxjava:2.1.10"
compile "io.reactivex.rxjava2:rxjava:2.1.14"

annotationProcessor "com.github.Raizlabs.DBFlow:dbflow-processor:${dbflow_version}"
compile "com.github.Raizlabs.DBFlow:dbflow-core:${dbflow_version}"
compile "com.github.Raizlabs.DBFlow:dbflow:${dbflow_version}"
compile "com.github.Raizlabs.DBFlow:dbflow-rx2:${dbflow_version}"

compile 'org.whispersystems:signal-protocol-android:2.3.0'
compile 'com.github.stfalcon:chatkit:0.2.2'
}
11 changes: 6 additions & 5 deletions app/src/main/AndroidManifest.xml
Expand Up @@ -10,7 +10,8 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.LOCAL_MAC_ADDRESS" />

<!-- This app never uses location, but Android requires location to do a BLE scan?!
<!--
This app never uses location, but Android requires location to do a BLE scan?!
https://stackoverflow.com/questions/33045581/location-needs-to-be-enabled-for-bluetooth-low-energy-scanning-on-android-6-0
https://stackoverflow.com/questions/33043582/bluetooth-low-energy-startscan-on-android-6-0-does-not-find-devices/33045489#33045489
-->
Expand All @@ -34,7 +35,6 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<activity
android:name=".views.ConversationList"
android:label="@string/conversation_view_title"
Expand All @@ -45,16 +45,18 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".views.SettingsActivity"
android:label="@string/settings_title"
android:theme="@style/AppTheme.NoActionBar" />

<activity
android:name=".views.debug.RawMessageList"
android:label="@string/raw_message_view_title"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".views.NewIdentityActivity"
android:label="@string/create_identity_title"
android:theme="@style/AppTheme.NoActionBar" />

<service
android:name=".sync.bluetooth.BluetoothSyncService"
Expand All @@ -69,7 +71,6 @@
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>

<receiver
android:name=".sync.bluetooth.BluetoothSyncServiceManager"
android:enabled="true"
Expand Down
Expand Up @@ -5,5 +5,5 @@
@Database(name = NoiseDatabase.NAME, version = NoiseDatabase.VERSION, foreignKeyConstraintsEnforced = true)
public class NoiseDatabase {
public static final String NAME = "NoiseDatabase";
public static final int VERSION = 1;
public static final int VERSION = 2;
}
@@ -0,0 +1,70 @@
package com.alternativeinfrastructures.noise.models;

import com.alternativeinfrastructures.noise.NoiseDatabase;
import com.alternativeinfrastructures.noise.models.signal.TypeConverters;
import com.alternativeinfrastructures.noise.storage.IdentityAnnouncementMessage;
import com.alternativeinfrastructures.noise.storage.UnknownMessage;
import com.raizlabs.android.dbflow.annotation.Column;
import com.raizlabs.android.dbflow.annotation.ForeignKey;
import com.raizlabs.android.dbflow.annotation.Index;
import com.raizlabs.android.dbflow.annotation.PrimaryKey;
import com.raizlabs.android.dbflow.annotation.Table;
import com.raizlabs.android.dbflow.rx2.structure.BaseRXModel;

import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.KeyHelper;

import java.io.IOException;

import io.reactivex.Single;

@Table(database = NoiseDatabase.class)
public class LocalIdentity extends BaseRXModel {
public static boolean validUsername(String username) {
// TODO: Other constraints?
return username != null && username.length() >= 5 && username.length() <= 31;
}

public static Single<Boolean> createNew(String username) throws IOException, InvalidKeyException, UnknownMessage.PayloadTooLargeException {
LocalIdentity identity = new LocalIdentity();
identity.username = username;
identity.registrationId = KeyHelper.generateRegistrationId(true /*extendedRange*/);
identity.identityKeyPair = KeyHelper.generateIdentityKeyPair();
// TODO: Once there's support for multiple identities, increment the signedPreKeyId
identity.signedPreKey = KeyHelper.generateSignedPreKey(
identity.identityKeyPair, 0 /*signedPreKeyId*/);
identity.remoteIdentity = identity.createRemoteIdentity();

byte zeroBits = 20; // XXX

return IdentityAnnouncementMessage.createAndSignAsync(identity.remoteIdentity, zeroBits)
.flatMap((UnknownMessage) -> identity.save());
}

private RemoteIdentity createRemoteIdentity() {
// TODO: Is using the registrationId as the deviceId okay?
return new RemoteIdentity(username, registrationId, identityKeyPair.getPublicKey());
}

@Index
protected String username;
public String getUsername() { return username; }

protected int registrationId;
public int getRegistrationId() { return registrationId; }

@PrimaryKey
@Column(typeConverter = TypeConverters.IdentityKeyPairConverter.class)
protected IdentityKeyPair identityKeyPair;
public IdentityKeyPair getIdentityKeyPair() { return identityKeyPair; }

@Column(typeConverter = TypeConverters.SignedPreKeyRecordConverter.class)
protected SignedPreKeyRecord signedPreKey;

@ForeignKey
protected RemoteIdentity remoteIdentity;
public RemoteIdentity getRemoteIdentity() { return remoteIdentity; }
}
@@ -0,0 +1,64 @@
package com.alternativeinfrastructures.noise.models;

import com.alternativeinfrastructures.noise.NoiseDatabase;
import com.alternativeinfrastructures.noise.models.signal.TypeConverters;
import com.alternativeinfrastructures.noise.storage.IdentityAnnouncementMessage;
import com.alternativeinfrastructures.noise.storage.UnknownMessage;
import com.raizlabs.android.dbflow.annotation.Column;
import com.raizlabs.android.dbflow.annotation.Index;
import com.raizlabs.android.dbflow.annotation.PrimaryKey;
import com.raizlabs.android.dbflow.annotation.Table;
import com.raizlabs.android.dbflow.rx2.structure.BaseRXModel;

import org.whispersystems.libsignal.IdentityKey;

import java.io.IOException;

import io.reactivex.Single;

@Table(database = NoiseDatabase.class)
public class RemoteIdentity extends BaseRXModel {
// public static Single<RemoteIdentity> createIdentityAndAnnouncementAsync(
// String username, int deviceId, IdentityKey identityKey) throws UnknownMessage.PayloadTooLargeException, IOException {
// RemoteIdentity identity = new RemoteIdentity();
// identity.username = username;
// identity.deviceId = deviceId;
// identity.identityKey = identityKey;
// return IdentityAnnouncementMessage.createAndSignAsync(identity)
// .map((UnknownMessage message) -> identity);
// }

public RemoteIdentity(String username, int deviceId, IdentityKey identityKey) {
this.username = username;
this.deviceId = deviceId;
this.identityKey = identityKey;
}

public RemoteIdentity() {}

@Override
public boolean equals(Object object) {
RemoteIdentity other;
if (!(object instanceof RemoteIdentity))
return false;

other = (RemoteIdentity) object;
return (username.equals(other.username) &&
deviceId == other.deviceId &&
identityKey.equals(other.identityKey));
}

@Column
@Index
protected String username;
public String getUsername() { return username; }

@Column
protected int deviceId;
public int getDeviceId() { return deviceId; }

@PrimaryKey
@Column(typeConverter = TypeConverters.IdentityKeyConverter.class)
protected IdentityKey identityKey;
public IdentityKey getIdentityKey() { return identityKey; }
}
@@ -0,0 +1,25 @@
package com.alternativeinfrastructures.noise.models.signal;

import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.PreKeyStore;

// Ensuring prekeys are used exactly once is nontrivial without a central service. Fortunately, they're optional.
public class DummyPreKeyStore implements PreKeyStore {
@Override
public PreKeyRecord loadPreKey(int preKeyId) {
return null;
}

@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
}

@Override
public boolean containsPreKey(int preKeyId) {
return false;
}

@Override
public void removePreKey(int preKeyId) {
}
}
@@ -0,0 +1,56 @@
package com.alternativeinfrastructures.noise.models.signal;

import com.alternativeinfrastructures.noise.models.LocalIdentity;
import com.alternativeinfrastructures.noise.models.RemoteIdentity;
import com.alternativeinfrastructures.noise.models.RemoteIdentity_Table;
import com.raizlabs.android.dbflow.sql.language.SQLite;

import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.IdentityKeyStore;

public class NoiseIdentityKeyStore implements IdentityKeyStore {
// TODO: Cache the LocalIdentity
// Constructor grabs a copy of the Identity from the database
// Register for changes to the Identity
// This way we can avoid querying the database every time

// TODO: Support multiple local identities?
// Constructor can accept arguments to pick one

// TODO: Use Rx

// TODO: Use SQLCipher or similar to avoid putting private keys in the database in cleartext

@Override
public IdentityKeyPair getIdentityKeyPair() {
LocalIdentity identity = SQLite.select().from(LocalIdentity.class).querySingle();
return identity == null ? null : identity.getIdentityKeyPair();
}

@Override
public int getLocalRegistrationId() {
LocalIdentity identity = SQLite.select().from(LocalIdentity.class).querySingle();
return identity == null ? null : identity.getRegistrationId();
}

@Override
public void saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
// RemoteIdentity identity = new RemoteIdentity(address.getName(), address.getDeviceId(),
// identityKey);
// identity.save().subscribe();
// TODO: Do we actually need this? Identity discovery should happen automatically via IdentityAnnouncementMessage
// Once we have something running, see who calls this
// TODO: Identity lifetime management
// At what point is it okay to update an identity?
}

@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
RemoteIdentity identity = SQLite.select().from(RemoteIdentity.class).where(
RemoteIdentity_Table.username.eq(address.getName()),
RemoteIdentity_Table.deviceId.eq(address.getDeviceId())).querySingle();
return identity != null && identityKey.equals(identity.getIdentityKey());
}
}

0 comments on commit 56d072b

Please sign in to comment.