Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Private key transfer #1090

Merged
merged 16 commits into from
Nov 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ dependencies {
compile 'com.googlecode.libphonenumber:libphonenumber:8.8.5'
compile 'com.nineoldandroids:library:2.4.0'
compile 'com.google.zxing:core:3.3.0'
compile 'me.dm7.barcodescanner:zxing:1.9.8'
compile 'com.segment.backo:backo:1.0.0'
compile "org.igniterealtime.smack:smack-tcp:$smackVersion"
compile "org.igniterealtime.smack:smack-experimental:$smackVersion"
Expand Down
21 changes: 21 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="${applicationId}.permission.NOTIFICATION_ACTION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
Expand All @@ -56,6 +57,7 @@
<uses-feature android:name="android.hardware.sensor.proximity" android:required="false"/>
<uses-feature android:name="android.hardware.location.network" android:required="false"/>
<uses-feature android:name="android.hardware.location.gps" android:required="false"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>

<application android:name=".Kontalk"
android:label="@string/app_name"
Expand Down Expand Up @@ -230,6 +232,24 @@
android:value="org.kontalk.ui.ConversationsActivity" />
</activity>

<activity android:name=".ui.RegisterDeviceActivity"
android:label="@string/register_device_title"
android:parentActivityName=".ui.prefs.PreferencesActivity"
tools:targetApi="jelly_bean">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kontalk.ui.prefs.PreferencesActivity" />
</activity>

<activity android:name=".ui.ImportDeviceActivity"
android:label="@string/import_device_title"
android:parentActivityName=".ui.NumberValidation"
tools:targetApi="jelly_bean">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kontalk.ui.NumberValidation" />
</activity>

<activity android:name=".ui.NumberValidation"/>

<activity android:name=".ui.CodeValidation"/>
Expand Down Expand Up @@ -285,6 +305,7 @@
android:value="org.kontalk.ui.ComposeMessage" />
</activity>

<activity android:name=".ui.ScanTextActivity" />

<!--
<activity android:name=".ui.QuickReplyActivity"
Expand Down
28 changes: 25 additions & 3 deletions app/src/main/java/org/kontalk/authenticator/Authenticator.java
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,9 @@ public static void exportDefaultPersonalKey(Context ctx, OutputStream dest, Stri
AccountManager m = AccountManager.get(ctx);
Account acc = getDefaultAccount(m);

String privKeyData = m.getUserData(acc, DATA_PRIVATEKEY);
byte[] privateKey = Base64.decode(privKeyData, Base64.DEFAULT);

byte[] privateKey = getPrivateKeyExportData(m, acc, passphrase, exportPassphrase);
byte[] bridgeCert = null;

if (bridgeCertificate) {
// bridge certificate is just plain data
String bridgeCertData = m.getUserData(acc, DATA_BRIDGECERT);
Expand All @@ -213,6 +212,29 @@ public static void exportDefaultPersonalKey(Context ctx, OutputStream dest, Stri
exp.save(privateKey, publicKey, dest, passphrase, exportPassphrase, bridgeCert, trustedKeys, acc.name);
}

public static byte[] getPrivateKeyExportData(Context ctx, String passphrase, String exportPassphrase)
throws PGPException, IOException {
AccountManager m = AccountManager.get(ctx);
Account acc = getDefaultAccount(m);

return getPrivateKeyExportData(m, acc, passphrase, exportPassphrase);
}

private static byte[] getPrivateKeyExportData(AccountManager m, Account acc, String passphrase, String exportPassphrase)
throws PGPException, IOException {
String privKeyData = m.getUserData(acc, DATA_PRIVATEKEY);
byte[] privateKey = Base64.decode(privKeyData, Base64.DEFAULT);

// custom export passphrase -- re-encrypt private key
if (exportPassphrase != null) {
privateKey = PGP.copySecretKeyRingWithNewPassword(privateKey,
passphrase, exportPassphrase)
.getEncoded();
}

return privateKey;
}

public static void setDefaultPersonalKey(Context ctx, byte[] publicKeyData, byte[] privateKeyData,
byte[] bridgeCertData, String passphrase) {
AccountManager am = AccountManager.get(ctx);
Expand Down
7 changes: 5 additions & 2 deletions app/src/main/java/org/kontalk/client/EndpointServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,11 @@ public int hashCode() {

@Override
public String toString() {
if (mHost != null) {
return mNetwork + "|" + mHost + ":" + mPort;
if (mHost != null && (!mNetwork.equalsIgnoreCase(mHost) || mPort != DEFAULT_PORT)) {
String out = mNetwork + "|" + mHost;
if (mPort != DEFAULT_PORT)
out += ":" + mPort;
return out;
}
else {
return mNetwork;
Expand Down
99 changes: 96 additions & 3 deletions app/src/main/java/org/kontalk/client/NumberValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.StanzaIdFilter;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.XMPPError;
Expand Down Expand Up @@ -93,6 +94,8 @@ public class NumberValidator implements Runnable, ConnectionHelperListener {
private static final int STEP_AUTH_TOKEN = 2;
/** Login test for imported key */
private static final int STEP_LOGIN_TEST = 3;
/** Requesting private key to server */
private static final int STEP_REQUEST_KEY = 4;

public static final int ERROR_THROTTLING = 1;
public static final int ERROR_USER_EXISTS = 2;
Expand Down Expand Up @@ -148,6 +151,7 @@ public class NumberValidator implements Runnable, ConnectionHelperListener {

private byte[] mImportedPrivateKey;
private byte[] mImportedPublicKey;
private String mPrivateKeyToken;

@SuppressWarnings("WeakerAccess")
final XMPPConnectionHelper mConnector;
Expand All @@ -166,6 +170,12 @@ public class NumberValidator implements Runnable, ConnectionHelperListener {
/** This will used to store the server-indicated challenge. */
String mServerChallenge;

public NumberValidator(Context context, EndpointServer.EndpointServerProvider serverProvider,
String phone, String privateKeyToken) {
this(context, serverProvider, null, phone, null, null);
mPrivateKeyToken = privateKeyToken;
}

public NumberValidator(Context context, EndpointServer.EndpointServerProvider serverProvider,
String name, String phone, PersonalKey key, String passphrase) {
this(context, serverProvider, name, phone, key, passphrase, BRAND_IMAGE_VECTOR);
Expand Down Expand Up @@ -220,6 +230,10 @@ public void importKey(byte[] privateKeyData, byte[] publicKeyData) {
mImportedPublicKey = publicKeyData;
}

public String getPhone() {
return mPhone;
}

public EndpointServer getServer() {
return mConnector.getServer();
}
Expand Down Expand Up @@ -424,7 +438,7 @@ else if (mStep == STEP_AUTH_TOKEN) {
mName, mPassphrase);
}
else {
mKeyRing = PGPKeyPairRing.load(mImportedPrivateKey, mImportedPublicKey);
mKeyRing = PGPKeyPairRing.loadArmored(mImportedPrivateKey, mImportedPublicKey);
}

// bridge certificate for connection
Expand Down Expand Up @@ -494,7 +508,13 @@ else if (mStep == STEP_LOGIN_TEST) {

// generate keyring immediately
// needed for connection
mKeyRing = PGPKeyPairRing.load(mImportedPrivateKey, mImportedPublicKey);
try {
mKeyRing = PGPKeyPairRing.loadArmored(mImportedPrivateKey, mImportedPublicKey);
}
catch (IOException e) {
// try not armored
mKeyRing = PGPKeyPairRing.load(mImportedPrivateKey, mImportedPublicKey);
}

// bridge certificate for connection
mBridgeCert = X509Bridge.createCertificate(mKeyRing.publicKey,
Expand All @@ -518,6 +538,48 @@ else if (mStep == STEP_LOGIN_TEST) {
mListener.onAuthTokenReceived(this,
mKeyRing.secretKey.getEncoded(), mKeyRing.publicKey.getEncoded());
}

// request private key to server via secure token
else if (mStep == STEP_REQUEST_KEY) {
if (mPrivateKeyToken == null)
throw new AssertionError("requesting a private key with no secure token!");

// we don't need these
mKeyRing = null;
mBridgeCert = null;

// connect to server
initConnection();

// prepare final verification form
Stanza request = createPrivateKeyRequest();

XMPPConnection conn = mConnector.getConnection();
conn.addAsyncStanzaListener(new StanzaListener() {
public void processStanza(Stanza packet) {
IQ iq = (IQ) packet;
if (iq.getType() == IQ.Type.result) {
ExtensionElement _accountData = iq.getExtension(Account.ELEMENT_NAME, Account.NAMESPACE);
if (_accountData instanceof Account) {
Account accountData = (Account) _accountData;

byte[] privateKeyData = accountData.getPrivateKeyData();
byte[] publicKeyData = accountData.getPublicKeyData();
if (privateKeyData != null && privateKeyData.length > 0 &&
publicKeyData != null && publicKeyData.length > 0) {
mListener.onPrivateKeyReceived(NumberValidator.this, privateKeyData, publicKeyData);
return;
}
}
}

mListener.onPrivateKeyRequestFailed(NumberValidator.this, -1);
}
}, new StanzaIdFilter(request.getStanzaId()));

// send request packet
conn.sendStanza(request);
}
}
catch (Throwable e) {
ReportingManager.logException(e);
Expand Down Expand Up @@ -578,6 +640,12 @@ public void testImport() {
mThread = null;
}

public void requestPrivateKey() {
mStep = STEP_REQUEST_KEY;
// next start call will trigger the next condition
mThread = null;
}

private void initConnection() throws XMPPException, SmackException,
PGPException, KeyStoreException, NoSuchProviderException,
NoSuchAlgorithmException, CertificateException,
Expand All @@ -587,7 +655,14 @@ private void initConnection() throws XMPPException, SmackException,
mConnector.setListener(this);
PersonalKey key = null;
if (mImportedPrivateKey != null && mImportedPublicKey != null) {
PGPKeyPairRing ring = PGPKeyPairRing.load(mImportedPrivateKey, mImportedPublicKey);
PGPKeyPairRing ring;
try {
ring = PGPKeyPairRing.loadArmored(mImportedPrivateKey, mImportedPublicKey);
}
catch (IOException e) {
// try not armored
ring = PGPKeyPairRing.load(mImportedPrivateKey, mImportedPublicKey);
}
key = PersonalKey.load(ring.secretKey, ring.publicKey, mPassphrase, mBridgeCert);
}
else if (mKey != null) {
Expand Down Expand Up @@ -666,6 +741,18 @@ private Stanza createValidationForm() throws IOException {
return iq;
}

private Stanza createPrivateKeyRequest() {
Registration iq = new Registration();
iq.setType(IQ.Type.get);
iq.setTo(mConnector.getConnection().getServiceName());

Account account = new Account();
account.setPrivateKeyToken(mPrivateKeyToken);
iq.addExtension(account);

return iq;
}

/**
* Finds the appropriate brand image form field from the response to use.
* @param form the response form
Expand Down Expand Up @@ -743,6 +830,12 @@ public interface NumberValidatorListener {

/** Called if validation code has not been verified. */
void onAuthTokenFailed(NumberValidator v, int reason);

/** Called when receiving the private key. */
void onPrivateKeyReceived(NumberValidator v, byte[] privateKey, byte[] publicKey);

/** Called when request for the private key failed. */
void onPrivateKeyRequestFailed(NumberValidator v, int reason);
}

/** Handles special numbers not handled by libphonenumber. */
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/org/kontalk/client/SmackInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,13 @@ public static void initializeRegistration() {
// not moving these into configuration since they are not loaded often
ProviderManager.addIQProvider("query", "jabber:iq:register", new RegistrationProvider());
ProviderManager.addExtensionProvider("x", "jabber:x:data", new DataFormProvider());
ProviderManager.addExtensionProvider(Account.ELEMENT_NAME, Account.NAMESPACE, new Account.Provider());
}

public static void deinitializeRegistration() {
ProviderManager.removeIQProvider("query", "jabber:iq:register");
ProviderManager.removeExtensionProvider("x", "jabber:x:data");
ProviderManager.removeExtensionProvider(Account.ELEMENT_NAME, Account.NAMESPACE);
}

private static void disableSmackDefault() {
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/org/kontalk/crypto/PGP.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ public static final class PGPKeyPairRing {
}

public static PGPKeyPairRing load(byte[] privateKeyData, byte[] publicKeyData)
throws IOException, PGPException {
PGPPublicKeyRing publicKey = new PGPPublicKeyRing(publicKeyData, sFingerprintCalculator);
PGPSecretKeyRing secretKey = new PGPSecretKeyRing(privateKeyData, sFingerprintCalculator);
return new PGPKeyPairRing(publicKey, secretKey);
}

public static PGPKeyPairRing loadArmored(byte[] privateKeyData, byte[] publicKeyData)
throws IOException, PGPException {
ArmoredInputStream inPublic = new ArmoredInputStream(new ByteArrayInputStream(publicKeyData));
PGPPublicKeyRing publicKey = new PGPPublicKeyRing(inPublic, sFingerprintCalculator);
Expand Down
Loading