From d9659fc3667cff043c922c5132f52bd5defc36fe Mon Sep 17 00:00:00 2001 From: Christian Schneppe Date: Fri, 18 Nov 2016 22:31:58 +0100 Subject: parse omemo fingerprints from uris --- .../messenger/crypto/axolotl/AxolotlService.java | 27 +++++--- .../crypto/axolotl/FingerprintStatus.java | 14 ++++ .../crypto/axolotl/SQLiteAxolotlStore.java | 16 ++++- .../crypto/axolotl/XmppAxolotlSession.java | 2 +- .../messenger/persistance/DatabaseBackend.java | 31 +++++++-- .../messenger/services/XmppConnectionService.java | 25 +++++++ .../messenger/ui/StartConversationActivity.java | 19 ++++-- .../de/pixart/messenger/ui/TrustKeysActivity.java | 5 +- .../de/pixart/messenger/ui/VerifyOTRActivity.java | 5 +- .../java/de/pixart/messenger/utils/XmppUri.java | 76 +++++++++++++++++----- 10 files changed, 174 insertions(+), 46 deletions(-) (limited to 'src/main/java/de') diff --git a/src/main/java/de/pixart/messenger/crypto/axolotl/AxolotlService.java b/src/main/java/de/pixart/messenger/crypto/axolotl/AxolotlService.java index 384a19067..807ede503 100644 --- a/src/main/java/de/pixart/messenger/crypto/axolotl/AxolotlService.java +++ b/src/main/java/de/pixart/messenger/crypto/axolotl/AxolotlService.java @@ -98,6 +98,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return false; } + public void preVerifyFingerprint(Contact contact, String fingerprint) { + axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().toBareJid().toPreppedString(), fingerprint); + } + private static class AxolotlAddressMap { protected Map> map; protected final Object MAP_LOCK = new Object(); @@ -200,7 +204,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public void put(AxolotlAddress address, XmppAxolotlSession value) { super.put(address, value); value.setNotFresh(); - xmppConnectionService.syncRosterToDisk(account); + xmppConnectionService.syncRosterToDisk(account); //TODO why? } public void put(XmppAxolotlSession session) { @@ -417,7 +421,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void purgeKey(final String fingerprint) { - axolotlStore.setFingerprintTrust(fingerprint.replaceAll("\\s", ""), FingerprintStatus.createCompromised()); + axolotlStore.setFingerprintStatus(fingerprint.replaceAll("\\s", ""), FingerprintStatus.createCompromised()); } public void publishOwnDeviceIdIfNeeded() { @@ -689,7 +693,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void setFingerprintTrust(String fingerprint, FingerprintStatus status) { - axolotlStore.setFingerprintTrust(fingerprint, status); + axolotlStore.setFingerprintStatus(fingerprint, status); } private void verifySessionWithPEP(final XmppAxolotlSession session) { @@ -748,14 +752,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private void finishBuildingSessionsFromPEP(final AxolotlAddress address) { AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), 0); - if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) - && !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) { + Map own = fetchStatusMap.getAll(ownAddress); + Map remote = fetchStatusMap.getAll(address); + if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) { FetchStatus report = null; - if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.SUCCESS_VERIFIED) - | fetchStatusMap.getAll(address).containsValue(FetchStatus.SUCCESS_VERIFIED)) { + if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) { + report = FetchStatus.SUCCESS; + } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) { report = FetchStatus.SUCCESS_VERIFIED; - } else if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.ERROR) - || fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) { + } else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) { report = FetchStatus.ERROR; } mXmppConnectionService.keyStatusUpdated(report); @@ -811,7 +816,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (Config.X509_VERIFICATION) { verifySessionWithPEP(session); } else { - fetchStatusMap.put(address, FetchStatus.SUCCESS); + FingerprintStatus status = getFingerprintTrust(bundle.getIdentityKey().getFingerprint().replaceAll("\\s","")); + boolean verified = status != null && status.isVerified(); + fetchStatusMap.put(address, verified ? FetchStatus.SUCCESS_VERIFIED : FetchStatus.SUCCESS); finishBuildingSessionsFromPEP(address); } } catch (UntrustedIdentityException | InvalidKeyException e) { diff --git a/src/main/java/de/pixart/messenger/crypto/axolotl/FingerprintStatus.java b/src/main/java/de/pixart/messenger/crypto/axolotl/FingerprintStatus.java index c7939411e..db67617b9 100644 --- a/src/main/java/de/pixart/messenger/crypto/axolotl/FingerprintStatus.java +++ b/src/main/java/de/pixart/messenger/crypto/axolotl/FingerprintStatus.java @@ -114,6 +114,20 @@ public class FingerprintStatus { return status; } + public FingerprintStatus toVerified() { + FingerprintStatus status = new FingerprintStatus(); + status.active = active; + status.trust = Trust.VERIFIED; + return status; + } + + public static FingerprintStatus createInactiveVerified() { + final FingerprintStatus status = new FingerprintStatus(); + status.trust = Trust.VERIFIED; + status.active = false; + return status; + } + public enum Trust { COMPROMISED, UNDECIDED, diff --git a/src/main/java/de/pixart/messenger/crypto/axolotl/SQLiteAxolotlStore.java b/src/main/java/de/pixart/messenger/crypto/axolotl/SQLiteAxolotlStore.java index 66786084d..2980bf7cc 100644 --- a/src/main/java/de/pixart/messenger/crypto/axolotl/SQLiteAxolotlStore.java +++ b/src/main/java/de/pixart/messenger/crypto/axolotl/SQLiteAxolotlStore.java @@ -187,7 +187,15 @@ public class SQLiteAxolotlStore implements AxolotlStore { @Override public void saveIdentity(String name, IdentityKey identityKey) { if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) { - mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey); + String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); + FingerprintStatus status = getFingerprintStatus(fingerprint); + if (status == null) { + status = FingerprintStatus.createActiveUndecided(); //default for new keys + } else { + status = status.toActive(); + } + mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey, status); + trustCache.remove(fingerprint); } } @@ -214,7 +222,7 @@ public class SQLiteAxolotlStore implements AxolotlStore { return (fingerprint == null)? null : trustCache.get(fingerprint); } - public void setFingerprintTrust(String fingerprint, FingerprintStatus status) { + public void setFingerprintStatus(String fingerprint, FingerprintStatus status) { mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status); trustCache.remove(fingerprint); } @@ -430,4 +438,8 @@ public class SQLiteAxolotlStore implements AxolotlStore { public void removeSignedPreKey(int signedPreKeyId) { mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); } + + public void preVerifyFingerprint(Account account, String name, String fingerprint) { + mXmppConnectionService.databaseBackend.storePreVerification(account,name,fingerprint,FingerprintStatus.createInactiveVerified()); + } } diff --git a/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlSession.java b/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlSession.java index a98583ac2..c71ad92a9 100644 --- a/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlSession.java +++ b/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlSession.java @@ -73,7 +73,7 @@ public class XmppAxolotlSession { } protected void setTrust(FingerprintStatus status) { - sqLiteAxolotlStore.setFingerprintTrust(getFingerprint(), status); + sqLiteAxolotlStore.setFingerprintStatus(getFingerprint(), status); } protected FingerprintStatus getTrust() { diff --git a/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java b/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java index d892c76e3..4ce66edf8 100644 --- a/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java +++ b/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java @@ -1105,7 +1105,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { continue; } try { - identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT), 0)); + String key = cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)); + if (key != null) { + identityKeys.add(new IdentityKey(Base64.decode(key, Base64.DEFAULT), 0)); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Missing key (possibly preverified) in database for account" + account.getJid().toBareJid() + ", address: " + name); + } } catch (InvalidKeyException e) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); } @@ -1133,10 +1138,6 @@ public class DatabaseBackend extends SQLiteOpenHelper { ); } - private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) { - storeIdentityKey(account, name, own, fingerprint, base64Serialized, FingerprintStatus.createActiveUndecided()); - } - private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, FingerprintStatus status) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); @@ -1146,6 +1147,22 @@ public class DatabaseBackend extends SQLiteOpenHelper { values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint); values.put(SQLiteAxolotlStore.KEY, base64Serialized); values.putAll(status.toContentValues()); + String where = SQLiteAxolotlStore.ACCOUNT+"=? AND "+SQLiteAxolotlStore.NAME+"=? AND "+SQLiteAxolotlStore.FINGERPRINT+" =?"; + String[] whereArgs = {account.getUuid(),name,fingerprint}; + int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,values,where,whereArgs); + if (rows == 0) { + db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); + } + } + + public void storePreVerification(Account account, String name, String fingerprint, FingerprintStatus status) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + values.put(SQLiteAxolotlStore.NAME, name); + values.put(SQLiteAxolotlStore.OWN, 0); + values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint); + values.putAll(status.toContentValues()); db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); } @@ -1226,8 +1243,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { } } - public void storeIdentityKey(Account account, String name, IdentityKey identityKey) { - storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); + public void storeIdentityKey(Account account, String name, IdentityKey identityKey, FingerprintStatus status) { + storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT), status); } public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) { diff --git a/src/main/java/de/pixart/messenger/services/XmppConnectionService.java b/src/main/java/de/pixart/messenger/services/XmppConnectionService.java index f725831bb..5fb90d822 100644 --- a/src/main/java/de/pixart/messenger/services/XmppConnectionService.java +++ b/src/main/java/de/pixart/messenger/services/XmppConnectionService.java @@ -73,6 +73,7 @@ import de.pixart.messenger.R; import de.pixart.messenger.crypto.PgpDecryptionService; import de.pixart.messenger.crypto.PgpEngine; import de.pixart.messenger.crypto.axolotl.AxolotlService; +import de.pixart.messenger.crypto.axolotl.FingerprintStatus; import de.pixart.messenger.crypto.axolotl.XmppAxolotlMessage; import de.pixart.messenger.entities.Account; import de.pixart.messenger.entities.Blockable; @@ -111,6 +112,7 @@ import de.pixart.messenger.utils.PhoneHelper; import de.pixart.messenger.utils.ReplacingSerialSingleThreadExecutor; import de.pixart.messenger.utils.SerialSingleThreadExecutor; import de.pixart.messenger.utils.Xmlns; +import de.pixart.messenger.utils.XmppUri; import de.pixart.messenger.utils.video.MediaController; import de.pixart.messenger.xml.Element; import de.pixart.messenger.xmpp.OnBindListener; @@ -3746,6 +3748,29 @@ public class XmppConnectionService extends Service { }); } + public void verifyFingerprints(Contact contact, List fingerprints) { + boolean needsRosterWrite = false; + final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); + for(XmppUri.Fingerprint fp : fingerprints) { + if (fp.type == XmppUri.FingerprintType.OTR) { + needsRosterWrite |= contact.addOtrFingerprint(fp.fingerprint); + } else if (fp.type == XmppUri.FingerprintType.OMEMO) { + String fingerprint = "05"+fp.fingerprint.replaceAll("\\s",""); + FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint); + if (fingerprintStatus != null) { + if (!fingerprintStatus.isVerified()) { + axolotlService.setFingerprintTrust(fingerprint,fingerprintStatus.toVerified()); + } + } else { + axolotlService.preVerifyFingerprint(contact,fingerprint); + } + } + } + if (needsRosterWrite) { + syncRosterToDisk(contact.getAccount()); + } + } + public interface OnMamPreferencesFetched { void onPreferencesFetched(Element prefs); void onPreferencesFetchFailed(); diff --git a/src/main/java/de/pixart/messenger/ui/StartConversationActivity.java b/src/main/java/de/pixart/messenger/ui/StartConversationActivity.java index f163cef72..83ab06b62 100644 --- a/src/main/java/de/pixart/messenger/ui/StartConversationActivity.java +++ b/src/main/java/de/pixart/messenger/ui/StartConversationActivity.java @@ -396,11 +396,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU } @SuppressLint("InflateParams") - protected void showCreateContactDialog(final String prefilledJid, final String fingerprint) { + protected void showCreateContactDialog(final String prefilledJid, final Invite invite) { EnterJidDialog dialog = new EnterJidDialog( this, mKnownHosts, mActivatedAccounts, getString(R.string.create_contact), getString(R.string.create), - prefilledJid, null, fingerprint == null + prefilledJid, null, !invite.hasFingerprints() ); dialog.setOnEnterJidDialogPositiveListener(new EnterJidDialog.OnEnterJidDialogPositiveListener() { @@ -419,7 +419,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU if (contact.showInRoster()) { throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists)); } else { - contact.addOtrFingerprint(fingerprint); + //contact.addOtrFingerprint(fingerprint); xmppConnectionService.createContact(contact); switchToConversation(contact); return true; @@ -841,6 +841,10 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU } private boolean handleJid(Invite invite) { + Log.d(Config.LOGTAG,"handling invite for "+invite.getJid()); + for(XmppUri.Fingerprint fp : invite.getFingerprints()) { + Log.d(Config.LOGTAG,fp.toString()); + } List contacts = xmppConnectionService.findContacts(invite.getJid()); if (invite.isMuc()) { Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid()); @@ -852,16 +856,19 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU return false; } } else if (contacts.size() == 0) { - showCreateContactDialog(invite.getJid().toString(), invite.getFingerprint()); + showCreateContactDialog(invite.getJid().toString(), invite); return false; } else if (contacts.size() == 1) { Contact contact = contacts.get(0); - if (invite.getFingerprint() != null) { + if (invite.hasFingerprints()) { + xmppConnectionService.verifyFingerprints(contact,invite.getFingerprints()); + } + /*if (invite.getFingerprint() != null) { if (contact.addOtrFingerprint(invite.getFingerprint())) { Log.d(Config.LOGTAG, "added new fingerprint"); xmppConnectionService.syncRosterToDisk(contact.getAccount()); } - } + }*/ switchToConversation(contact); return true; } else { diff --git a/src/main/java/de/pixart/messenger/ui/TrustKeysActivity.java b/src/main/java/de/pixart/messenger/ui/TrustKeysActivity.java index dd499433a..2f3951c3a 100644 --- a/src/main/java/de/pixart/messenger/ui/TrustKeysActivity.java +++ b/src/main/java/de/pixart/messenger/ui/TrustKeysActivity.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import de.pixart.messenger.Config; import de.pixart.messenger.OmemoActivity; import de.pixart.messenger.R; import de.pixart.messenger.crypto.axolotl.AxolotlService; @@ -244,7 +245,9 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat Toast.makeText(TrustKeysActivity.this,R.string.error_fetching_omemo_key,Toast.LENGTH_SHORT).show(); break; case SUCCESS_VERIFIED: - Toast.makeText(TrustKeysActivity.this,R.string.verified_omemo_key_with_certificate,Toast.LENGTH_LONG).show(); + Toast.makeText(TrustKeysActivity.this, + Config.X509_VERIFICATION ? R.string.verified_omemo_key_with_certificate : R.string.all_omemo_keys_have_been_verified, + Toast.LENGTH_LONG).show(); break; } } diff --git a/src/main/java/de/pixart/messenger/ui/VerifyOTRActivity.java b/src/main/java/de/pixart/messenger/ui/VerifyOTRActivity.java index 9ef4ca171..bee5ab692 100644 --- a/src/main/java/de/pixart/messenger/ui/VerifyOTRActivity.java +++ b/src/main/java/de/pixart/messenger/ui/VerifyOTRActivity.java @@ -173,11 +173,10 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer protected boolean verifyWithUri(XmppUri uri) { Contact contact = mConversation.getContact(); - if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.getFingerprint() != null) { - contact.addOtrFingerprint(uri.getFingerprint()); + if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.hasFingerprints()) { + xmppConnectionService.verifyFingerprints(contact,uri.getFingerprints()); Toast.makeText(this,R.string.verified,Toast.LENGTH_SHORT).show(); updateView(); - xmppConnectionService.syncRosterToDisk(contact.getAccount()); return true; } else { Toast.makeText(this,R.string.could_not_verify_fingerprint,Toast.LENGTH_SHORT).show(); diff --git a/src/main/java/de/pixart/messenger/utils/XmppUri.java b/src/main/java/de/pixart/messenger/utils/XmppUri.java index 832b4018f..31ba694ff 100644 --- a/src/main/java/de/pixart/messenger/utils/XmppUri.java +++ b/src/main/java/de/pixart/messenger/utils/XmppUri.java @@ -4,7 +4,9 @@ import android.net.Uri; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.util.ArrayList; import java.util.List; +import java.util.Locale; import de.pixart.messenger.xmpp.jid.InvalidJidException; import de.pixart.messenger.xmpp.jid.Jid; @@ -14,6 +16,8 @@ public class XmppUri { protected String jid; protected boolean muc; protected String fingerprint; + protected List fingerprints = new ArrayList<>(); + private static final String OMEMO_URI_PARAM = "omemo-sid-"; public XmppUri(String uri) { try { @@ -31,10 +35,10 @@ public class XmppUri { parse(uri); } - public static boolean isXmppUri(String uri) { - String scheme = Uri.parse(uri).getScheme(); - return "xmpp".equalsIgnoreCase(scheme); - } + public static boolean isXmppUri(String uri) { + String scheme = Uri.parse(uri).getScheme(); + return "xmpp".equalsIgnoreCase(scheme); + } protected void parse(Uri uri) { String scheme = uri.getScheme(); @@ -61,7 +65,7 @@ public class XmppUri { } else { jid = uri.getSchemeSpecificPart().split("\\?")[0]; } - fingerprint = parseFingerprint(uri.getQuery()); + this.fingerprints = parseFingerprints(uri.getQuery()); } else if ("imto".equalsIgnoreCase(scheme)) { // sample: imto://xmpp/foo@bar.com try { @@ -78,18 +82,29 @@ public class XmppUri { } } - protected String parseFingerprint(String query) { - if (query == null) { - return null; - } else { - final String NEEDLE = "otr-fingerprint="; - int index = query.indexOf(NEEDLE); - if (index >= 0 && query.length() >= (NEEDLE.length() + index + 40)) { - return query.substring(index + NEEDLE.length(), index + NEEDLE.length() + 40); - } else { + protected List parseFingerprints(String query) { + List fingerprints = new ArrayList<>(); + String[] pairs = query == null ? new String[0] : query.split(";"); + for(String pair : pairs) { + String[] parts = pair.split("=",2); + if (parts.length == 2) { + String key = parts[0].toLowerCase(Locale.US); + String value = parts[1]; + if ("otr-fingerprint".equals(key)) { + fingerprints.add(new Fingerprint(FingerprintType.OTR,value)); + } + if (key.startsWith(OMEMO_URI_PARAM)) { + try { + int id = Integer.parseInt(key.substring(OMEMO_URI_PARAM.length())); + fingerprints.add(new Fingerprint(FingerprintType.OMEMO,value,id)); + } catch (Exception e) { + //ignoring invalid device id + } + } return null; } } + return fingerprints; } public Jid getJid() { @@ -100,7 +115,36 @@ public class XmppUri { } } - public String getFingerprint() { - return this.fingerprint; + public List getFingerprints() { + return this.fingerprints; + } + + public boolean hasFingerprints() { + return fingerprints.size() > 0; + } + public enum FingerprintType { + OMEMO, + OTR + } + + public static class Fingerprint { + public final FingerprintType type; + public final String fingerprint; + public final int deviceId; + + public Fingerprint(FingerprintType type, String fingerprint) { + this(type, fingerprint, 0); + } + + public Fingerprint(FingerprintType type, String fingerprint, int deviceId) { + this.type = type; + this.fingerprint = fingerprint; + this.deviceId = deviceId; + } + + @Override + public String toString() { + return type.toString()+": "+fingerprint+(deviceId != 0 ? " "+String.valueOf(deviceId) : ""); + } } } -- cgit v1.2.3