From f0c22d593f60c2142e895d5cce41b4fb916d6b2f Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Sun, 13 Jul 2014 15:21:41 -0700 Subject: [PATCH] Simplify/clarify internal interfaces and introduce optional types. --- .../test/SessionBuilderTest.java | 29 +- .../test/SessionCipherTest.java | 70 ++- .../test/ratchet/RatchetingSessionTest.java | 36 +- .../test/ratchet/VerifyKeyTest.java | 13 +- .../libaxolotl/SessionBuilder.java | 178 +++---- .../libaxolotl/ratchet/RatchetingSession.java | 274 ++++++++--- .../libaxolotl/ratchet/VerifyKey.java | 17 +- .../libaxolotl/util/guava/Absent.java | 87 ++++ .../libaxolotl/util/guava/Function.java | 61 +++ .../libaxolotl/util/guava/Optional.java | 232 +++++++++ .../libaxolotl/util/guava/Preconditions.java | 447 ++++++++++++++++++ .../libaxolotl/util/guava/Present.java | 88 ++++ .../libaxolotl/util/guava/Supplier.java | 36 ++ 13 files changed, 1364 insertions(+), 204 deletions(-) create mode 100644 libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Absent.java create mode 100644 libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Function.java create mode 100644 libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Optional.java create mode 100644 libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Preconditions.java create mode 100644 libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Present.java create mode 100644 libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Supplier.java diff --git a/libaxolotl/src/androidTest/java/org/whispersystems/test/SessionBuilderTest.java b/libaxolotl/src/androidTest/java/org/whispersystems/test/SessionBuilderTest.java index a974cd1d1b..683103eb36 100644 --- a/libaxolotl/src/androidTest/java/org/whispersystems/test/SessionBuilderTest.java +++ b/libaxolotl/src/androidTest/java/org/whispersystems/test/SessionBuilderTest.java @@ -426,23 +426,23 @@ public class SessionBuilderTest extends AndroidTestCase { } public void testBadVerificationTagV3() throws InvalidKeyException, UntrustedIdentityException, InvalidVersionException, InvalidMessageException, InvalidKeyIdException, DuplicateMessageException, LegacyMessageException { - SessionStore aliceSessionStore = new InMemorySessionStore(); + SessionStore aliceSessionStore = new InMemorySessionStore(); SignedPreKeyStore aliceSignedPreKeyStore = new InMemorySignedPreKeyStore(); - PreKeyStore alicePreKeyStore = new InMemoryPreKeyStore(); - IdentityKeyStore aliceIdentityKeyStore = new InMemoryIdentityKeyStore(); - SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceSessionStore, alicePreKeyStore, - aliceSignedPreKeyStore, - aliceIdentityKeyStore, - BOB_RECIPIENT_ID, 1); + PreKeyStore alicePreKeyStore = new InMemoryPreKeyStore(); + IdentityKeyStore aliceIdentityKeyStore = new InMemoryIdentityKeyStore(); + SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceSessionStore, alicePreKeyStore, + aliceSignedPreKeyStore, + aliceIdentityKeyStore, + BOB_RECIPIENT_ID, 1); - SessionStore bobSessionStore = new InMemorySessionStore(); - PreKeyStore bobPreKeyStore = new InMemoryPreKeyStore(); + SessionStore bobSessionStore = new InMemorySessionStore(); + PreKeyStore bobPreKeyStore = new InMemoryPreKeyStore(); SignedPreKeyStore bobSignedPreKeyStore = new InMemorySignedPreKeyStore(); - IdentityKeyStore bobIdentityKeyStore = new InMemoryIdentityKeyStore(); - SessionBuilder bobSessionBuilder = new SessionBuilder(bobSessionStore, bobPreKeyStore, - bobSignedPreKeyStore, - bobIdentityKeyStore, - ALICE_RECIPIENT_ID, 1); + IdentityKeyStore bobIdentityKeyStore = new InMemoryIdentityKeyStore(); + SessionBuilder bobSessionBuilder = new SessionBuilder(bobSessionStore, bobPreKeyStore, + bobSignedPreKeyStore, + bobIdentityKeyStore, + ALICE_RECIPIENT_ID, 1); ECKeyPair bobPreKeyPair = Curve.generateKeyPair(true); ECKeyPair bobSignedPreKeyPair = Curve.generateKeyPair(true); @@ -469,6 +469,7 @@ public class SessionBuilderTest extends AndroidTestCase { for (int i=0;iabsent()) + .setTheirIdentityKey(bobIdentityKey.getPublicKey()) + .setTheirBaseKey(bobBaseKey.getPublicKey()) + .setTheirEphemeralKey(bobEphemeralKey.getPublicKey()) + .setTheirPreKey(Optional.absent()) + .create(); - RatchetingSession.initializeSession(aliceSessionState, 2, aliceBaseKey, bobBaseKey.getPublicKey(), - aliceEphemeralKey, bobEphemeralKey.getPublicKey(), - null, null, - aliceIdentityKey, bobIdentityKey.getPublicKey()); + InitializationParameters bobParameters = + InitializationParameters.newBuilder() + .setOurIdentityKey(bobIdentityKey) + .setOurBaseKey(bobBaseKey) + .setOurEphemeralKey(bobEphemeralKey) + .setOurPreKey(Optional.absent()) + .setTheirIdentityKey(aliceIdentityKey.getPublicKey()) + .setTheirBaseKey(aliceBaseKey.getPublicKey()) + .setTheirEphemeralKey(aliceEphemeralKey.getPublicKey()) + .setTheirPreKey(Optional.absent()) + .create(); - RatchetingSession.initializeSession(bobSessionState, 2, bobBaseKey, aliceBaseKey.getPublicKey(), - bobEphemeralKey, aliceEphemeralKey.getPublicKey(), - null, null, - bobIdentityKey, aliceIdentityKey.getPublicKey()); + + RatchetingSession.initializeSession(aliceSessionState, 2, aliceParameters); + RatchetingSession.initializeSession(bobSessionState, 2, bobParameters); } private void initializeSessionsV3(SessionState aliceSessionState, SessionState bobSessionState) @@ -165,16 +186,33 @@ public class SessionCipherTest extends AndroidTestCase { ECKeyPair bobPreKey = Curve.generateKeyPair(true); + InitializationParameters aliceParameters = + InitializationParameters.newBuilder() + .setOurIdentityKey(aliceIdentityKey) + .setOurBaseKey(aliceBaseKey) + .setOurEphemeralKey(aliceEphemeralKey) + .setOurPreKey(Optional.of(alicePreKey)) + .setTheirIdentityKey(bobIdentityKey.getPublicKey()) + .setTheirBaseKey(bobBaseKey.getPublicKey()) + .setTheirEphemeralKey(bobEphemeralKey.getPublicKey()) + .setTheirPreKey(Optional.of(bobPreKey.getPublicKey())) + .create(); - RatchetingSession.initializeSession(aliceSessionState, 3, aliceBaseKey, bobBaseKey.getPublicKey(), - aliceEphemeralKey, bobEphemeralKey.getPublicKey(), - alicePreKey, bobPreKey.getPublicKey(), - aliceIdentityKey, bobIdentityKey.getPublicKey()); + InitializationParameters bobParameters = + InitializationParameters.newBuilder() + .setOurIdentityKey(bobIdentityKey) + .setOurBaseKey(bobBaseKey) + .setOurEphemeralKey(bobEphemeralKey) + .setOurPreKey(Optional.of(bobPreKey)) + .setTheirIdentityKey(aliceIdentityKey.getPublicKey()) + .setTheirBaseKey(aliceBaseKey.getPublicKey()) + .setTheirEphemeralKey(aliceEphemeralKey.getPublicKey()) + .setTheirPreKey(Optional.of(alicePreKey.getPublicKey())) + .create(); - RatchetingSession.initializeSession(bobSessionState, 3, bobBaseKey, aliceBaseKey.getPublicKey(), - bobEphemeralKey, aliceEphemeralKey.getPublicKey(), - bobPreKey, alicePreKey.getPublicKey(), - bobIdentityKey, aliceIdentityKey.getPublicKey()); + + RatchetingSession.initializeSession(aliceSessionState, 3, aliceParameters); + RatchetingSession.initializeSession(bobSessionState, 3, bobParameters); } } diff --git a/libaxolotl/src/androidTest/java/org/whispersystems/test/ratchet/RatchetingSessionTest.java b/libaxolotl/src/androidTest/java/org/whispersystems/test/ratchet/RatchetingSessionTest.java index b9e088b3fa..8c59cba8e6 100644 --- a/libaxolotl/src/androidTest/java/org/whispersystems/test/ratchet/RatchetingSessionTest.java +++ b/libaxolotl/src/androidTest/java/org/whispersystems/test/ratchet/RatchetingSessionTest.java @@ -11,9 +11,12 @@ import org.whispersystems.libaxolotl.ecc.ECPrivateKey; import org.whispersystems.libaxolotl.ecc.ECPublicKey; import org.whispersystems.libaxolotl.ratchet.RatchetingSession; import org.whispersystems.libaxolotl.state.SessionState; +import org.whispersystems.libaxolotl.util.guava.Optional; import java.util.Arrays; +import static org.whispersystems.libaxolotl.ratchet.RatchetingSession.InitializationParameters; + public class RatchetingSessionTest extends AndroidTestCase { public void testRatchetingSessionAsBob() throws InvalidKeyException { @@ -105,12 +108,21 @@ public class RatchetingSessionTest extends AndroidTestCase { ECPublicKey aliceEphemeralPublicKey = Curve.decodePoint(aliceEphemeralPublic, 0); IdentityKey aliceIdentityPublicKey = new IdentityKey(aliceIdentityPublic, 0); + InitializationParameters parameters = InitializationParameters.newBuilder() + .setOurBaseKey(bobBaseKey) + .setOurEphemeralKey(bobEphemeralKey) + .setOurIdentityKey(bobIdentityKey) + .setOurPreKey(Optional.absent()) + .setTheirBaseKey(aliceBasePublicKey) + .setTheirEphemeralKey(aliceEphemeralPublicKey) + .setTheirIdentityKey(aliceIdentityPublicKey) + .setTheirPreKey(Optional.absent()) + .create(); + SessionState session = new SessionState(); - RatchetingSession.initializeSession(session, 2, bobBaseKey, aliceBasePublicKey, - bobEphemeralKey, aliceEphemeralPublicKey, - null, null, - bobIdentityKey, aliceIdentityPublicKey); + + RatchetingSession.initializeSession(session, 2, parameters); assertTrue(session.getLocalIdentityKey().equals(bobIdentityKey.getPublicKey())); assertTrue(session.getRemoteIdentityKey().equals(aliceIdentityPublicKey)); @@ -205,10 +217,18 @@ public class RatchetingSessionTest extends AndroidTestCase { SessionState session = new SessionState(); - RatchetingSession.initializeSession(session, 2, aliceBaseKey, bobBasePublicKey, - aliceEphemeralKey, bobEphemeralPublicKey, - null, null, - aliceIdentityKey, bobIdentityKey); + InitializationParameters parameters = InitializationParameters.newBuilder() + .setOurBaseKey(aliceBaseKey) + .setOurEphemeralKey(aliceEphemeralKey) + .setOurIdentityKey(aliceIdentityKey) + .setOurPreKey(Optional.absent()) + .setTheirBaseKey(bobBasePublicKey) + .setTheirEphemeralKey(bobEphemeralPublicKey) + .setTheirIdentityKey(bobIdentityKey) + .setTheirPreKey(Optional.absent()) + .create(); + + RatchetingSession.initializeSession(session, 2, parameters); assertTrue(session.getLocalIdentityKey().equals(aliceIdentityKey.getPublicKey())); assertTrue(session.getRemoteIdentityKey().equals(bobIdentityKey)); diff --git a/libaxolotl/src/androidTest/java/org/whispersystems/test/ratchet/VerifyKeyTest.java b/libaxolotl/src/androidTest/java/org/whispersystems/test/ratchet/VerifyKeyTest.java index dc1a34b0ad..11495e2ff6 100644 --- a/libaxolotl/src/androidTest/java/org/whispersystems/test/ratchet/VerifyKeyTest.java +++ b/libaxolotl/src/androidTest/java/org/whispersystems/test/ratchet/VerifyKeyTest.java @@ -1,11 +1,14 @@ package org.whispersystems.test.ratchet; import android.test.AndroidTestCase; +import android.util.Log; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.ecc.Curve; import org.whispersystems.libaxolotl.ecc.ECPublicKey; import org.whispersystems.libaxolotl.ratchet.VerifyKey; +import org.whispersystems.libaxolotl.util.Hex; +import org.whispersystems.libaxolotl.util.guava.Optional; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -62,9 +65,8 @@ public class VerifyKeyTest extends AndroidTestCase { (byte)0x79, (byte)0x00, (byte)0xef, (byte)0x1b, (byte)0x40, (byte)0x0f, (byte)0xdc}; - byte[] expectedTag = {(byte)0x68, (byte)0x64, (byte)0xbf, (byte)0xbf, (byte)0x82, - (byte)0x34, (byte)0x20, (byte)0xdc}; - + byte[] expectedTag = {(byte)0x2f, (byte)0x77, (byte)0xaf, (byte)0xad, (byte)0x5b, + (byte)0x96, (byte)0xf5, (byte)0x3c}; ECPublicKey aliceBaseKey = Curve.decodePoint(aliceBaseKeyBytes, 0); ECPublicKey alicePreKey = aliceBaseKey; @@ -75,8 +77,9 @@ public class VerifyKeyTest extends AndroidTestCase { ECPublicKey bobIdentityKey = Curve.decodePoint(bobIdentityKeyBytes, 0); VerifyKey verifyKey = new VerifyKey(key); - byte[] verification = verifyKey.generateVerification(aliceBaseKey, alicePreKey, aliceIdentityKey, - bobBaseKey, bobPreKey, bobIdentityKey); + byte[] verification = verifyKey.generateVerification(aliceBaseKey, Optional.of(alicePreKey), + aliceIdentityKey, bobBaseKey, + Optional.of(bobPreKey), bobIdentityKey); assertTrue(MessageDigest.isEqual(verification, expectedTag)); } diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionBuilder.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionBuilder.java index f2d5836c11..158cdd165e 100644 --- a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionBuilder.java +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionBuilder.java @@ -9,19 +9,22 @@ import org.whispersystems.libaxolotl.protocol.CiphertextMessage; import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage; import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; import org.whispersystems.libaxolotl.ratchet.RatchetingSession; -import org.whispersystems.libaxolotl.state.SignedPreKeyStore; import org.whispersystems.libaxolotl.state.IdentityKeyStore; import org.whispersystems.libaxolotl.state.PreKeyBundle; -import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.PreKeyStore; import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SessionState; import org.whispersystems.libaxolotl.state.SessionStore; +import org.whispersystems.libaxolotl.state.SignedPreKeyStore; +import org.whispersystems.libaxolotl.util.Hex; import org.whispersystems.libaxolotl.util.KeyHelper; import org.whispersystems.libaxolotl.util.Medium; +import org.whispersystems.libaxolotl.util.guava.Optional; import java.security.MessageDigest; +import static org.whispersystems.libaxolotl.ratchet.RatchetingSession.InitializationParameters; + /** * SessionBuilder is responsible for setting up encrypted sessions. * Once a session has been established, {@link org.whispersystems.libaxolotl.SessionCipher} @@ -107,40 +110,33 @@ public class SessionBuilder { private void processV3(PreKeyWhisperMessage message) throws UntrustedIdentityException, InvalidKeyIdException, InvalidKeyException { - SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId); - int preKeyId = message.getPreKeyId(); - int signedPreKeyId = message.getSignedPreKeyId(); - ECPublicKey theirBaseKey = message.getBaseKey(); - ECPublicKey theirEphemeralKey = message.getWhisperMessage().getSenderEphemeral(); - IdentityKey theirIdentityKey = message.getIdentityKey(); + SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId); - if (sessionRecord.hasSessionState(message.getMessageVersion(), theirBaseKey.serialize())) { + if (sessionRecord.hasSessionState(message.getMessageVersion(), message.getBaseKey().serialize())) { Log.w(TAG, "We've already setup a session for this V3 message, letting bundled message fall through..."); return; } - if (preKeyId >=0 && !preKeyStore.containsPreKey(preKeyId)) - throw new InvalidKeyIdException("No such prekey: " + preKeyId); + boolean simultaneousInitiate = sessionRecord.getSessionState().hasPendingPreKey(); - if (!signedPreKeyStore.containsSignedPreKey(signedPreKeyId)) - throw new InvalidKeyIdException("No such device key: " + signedPreKeyId); + InitializationParameters.Builder parameters = InitializationParameters.newBuilder(); - ECKeyPair ourBaseKey = signedPreKeyStore.loadSignedPreKey(signedPreKeyId).getKeyPair(); - ECKeyPair ourEphemeralKey = ourBaseKey; - ECKeyPair ourPreKey = preKeyId < 0 ? ourBaseKey : preKeyStore.loadPreKey(preKeyId).getKeyPair(); - ECPublicKey theirPreKey = theirBaseKey; - IdentityKeyPair ourIdentityKey = identityKeyStore.getIdentityKeyPair(); - boolean simultaneousInitiate = sessionRecord.getSessionState().hasPendingPreKey(); + parameters.setTheirBaseKey(message.getBaseKey()); + parameters.setTheirEphemeralKey(message.getWhisperMessage().getSenderEphemeral()); + parameters.setTheirPreKey(Optional.of(message.getBaseKey())); + parameters.setTheirIdentityKey(message.getIdentityKey()); + + parameters.setOurBaseKey(signedPreKeyStore.loadSignedPreKey(message.getSignedPreKeyId()).getKeyPair()); + parameters.setOurEphemeralKey(parameters.getOurBaseKey()); + parameters.setOurIdentityKey(identityKeyStore.getIdentityKeyPair()); + + if (message.getPreKeyId() >= 0) parameters.setOurPreKey(Optional.of(preKeyStore.loadPreKey(message.getPreKeyId()).getKeyPair())); + else parameters.setOurPreKey(Optional.absent()); if (!simultaneousInitiate) sessionRecord.reset(); else sessionRecord.archiveCurrentState(); - RatchetingSession.initializeSession(sessionRecord.getSessionState(), - message.getMessageVersion(), - ourBaseKey, theirBaseKey, - ourEphemeralKey, theirEphemeralKey, - ourPreKey, theirPreKey, - ourIdentityKey, theirIdentityKey); + RatchetingSession.initializeSession(sessionRecord.getSessionState(), message.getMessageVersion(), parameters.create()); if (!MessageDigest.isEqual(sessionRecord.getSessionState().getVerification(), message.getVerification())) { throw new InvalidKeyException("Verification secret mismatch!"); @@ -148,51 +144,47 @@ public class SessionBuilder { sessionRecord.getSessionState().setLocalRegistrationId(identityKeyStore.getLocalRegistrationId()); sessionRecord.getSessionState().setRemoteRegistrationId(message.getRegistrationId()); - sessionRecord.getSessionState().setAliceBaseKey(theirBaseKey.serialize()); + sessionRecord.getSessionState().setAliceBaseKey(message.getBaseKey().serialize()); if (simultaneousInitiate) sessionRecord.getSessionState().setNeedsRefresh(true); sessionStore.storeSession(recipientId, deviceId, sessionRecord); - if (preKeyId >= 0 && preKeyId != Medium.MAX_VALUE) { - preKeyStore.removePreKey(preKeyId); + if (message.getPreKeyId() >= 0 && message.getPreKeyId() != Medium.MAX_VALUE) { + preKeyStore.removePreKey(message.getPreKeyId()); } } private void processV2(PreKeyWhisperMessage message) throws UntrustedIdentityException, InvalidKeyIdException, InvalidKeyException { - int preKeyId = message.getPreKeyId(); - ECPublicKey theirBaseKey = message.getBaseKey(); - ECPublicKey theirEphemeralKey = message.getWhisperMessage().getSenderEphemeral(); - IdentityKey theirIdentityKey = message.getIdentityKey(); - if (!preKeyStore.containsPreKey(preKeyId) && + if (!preKeyStore.containsPreKey(message.getPreKeyId()) && sessionStore.containsSession(recipientId, deviceId)) { Log.w(TAG, "We've already processed the prekey part of this V2 session, letting bundled message fall through..."); return; } - if (!preKeyStore.containsPreKey(preKeyId)) - throw new InvalidKeyIdException("No such prekey: " + preKeyId); + SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId); + boolean simultaneousInitiate = sessionRecord.getSessionState().hasPendingPreKey(); - SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId); - PreKeyRecord preKeyRecord = preKeyStore.loadPreKey(preKeyId); - ECKeyPair ourBaseKey = preKeyRecord.getKeyPair(); - ECKeyPair ourEphemeralKey = ourBaseKey; - IdentityKeyPair ourIdentityKey = identityKeyStore.getIdentityKeyPair(); - boolean simultaneousInitiate = sessionRecord.getSessionState().hasPendingPreKey(); + InitializationParameters.Builder parameters = InitializationParameters.newBuilder(); + + parameters.setTheirBaseKey(message.getBaseKey()); + parameters.setTheirEphemeralKey(message.getWhisperMessage().getSenderEphemeral()); + parameters.setTheirPreKey(Optional.absent()); + parameters.setTheirIdentityKey(message.getIdentityKey()); + + parameters.setOurBaseKey(preKeyStore.loadPreKey(message.getPreKeyId()).getKeyPair()); + parameters.setOurEphemeralKey(parameters.getOurBaseKey()); + parameters.setOurPreKey(Optional.absent()); + parameters.setOurIdentityKey(identityKeyStore.getIdentityKeyPair()); if (!simultaneousInitiate) sessionRecord.reset(); else sessionRecord.archiveCurrentState(); - RatchetingSession.initializeSession(sessionRecord.getSessionState(), - message.getMessageVersion(), - ourBaseKey, theirBaseKey, - ourEphemeralKey, theirEphemeralKey, - null, null, - ourIdentityKey, theirIdentityKey); + RatchetingSession.initializeSession(sessionRecord.getSessionState(), message.getMessageVersion(), parameters.create()); sessionRecord.getSessionState().setLocalRegistrationId(identityKeyStore.getLocalRegistrationId()); sessionRecord.getSessionState().setRemoteRegistrationId(message.getRegistrationId()); @@ -201,8 +193,8 @@ public class SessionBuilder { sessionStore.storeSession(recipientId, deviceId, sessionRecord); - if (preKeyId != Medium.MAX_VALUE) { - preKeyStore.removePreKey(preKeyId); + if (message.getPreKeyId() != Medium.MAX_VALUE) { + preKeyStore.removePreKey(message.getPreKeyId()); } } @@ -218,6 +210,7 @@ public class SessionBuilder { * trusted. */ public void process(PreKeyBundle preKey) throws InvalidKeyException, UntrustedIdentityException { + if (!identityKeyStore.isTrustedIdentity(recipientId, preKey.getIdentityKey())) { throw new UntrustedIdentityException(); } @@ -230,25 +223,31 @@ public class SessionBuilder { throw new InvalidKeyException("Invalid signature on device key!"); } - SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId); - IdentityKeyPair ourIdentityKey = identityKeyStore.getIdentityKeyPair(); - ECKeyPair ourBaseKey = Curve.generateKeyPair(true); - ECKeyPair ourEphemeralKey = Curve.generateKeyPair(true); - ECKeyPair ourPreKey = ourBaseKey; + if (preKey.getSignedPreKey() == null && preKey.getPreKey() == null) { + throw new InvalidKeyException("Both signed and unsigned prekeys are absent!"); + } - IdentityKey theirIdentityKey = preKey.getIdentityKey(); - ECPublicKey theirPreKey = preKey.getPreKey(); - ECPublicKey theirBaseKey = preKey.getSignedPreKey() == null ? preKey.getPreKey() : preKey.getSignedPreKey(); - ECPublicKey theirEphemeralKey = theirBaseKey; + SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId); + InitializationParameters.Builder parameters = InitializationParameters.newBuilder(); + ECKeyPair ourBaseKey = Curve.generateKeyPair(true); + + parameters.setOurIdentityKey(identityKeyStore.getIdentityKeyPair()); + parameters.setOurBaseKey(ourBaseKey); + parameters.setOurEphemeralKey(Curve.generateKeyPair(true)); + parameters.setOurPreKey(Optional.of(parameters.getOurBaseKey())); + + parameters.setTheirIdentityKey(preKey.getIdentityKey()); + if (preKey.getSignedPreKey() == null) parameters.setTheirBaseKey(preKey.getPreKey()); + else parameters.setTheirBaseKey(preKey.getSignedPreKey()); + parameters.setTheirEphemeralKey(parameters.getTheirBaseKey()); + parameters.setTheirPreKey(Optional.fromNullable(preKey.getPreKey())); if (sessionRecord.getSessionState().getNeedsRefresh()) sessionRecord.archiveCurrentState(); else sessionRecord.reset(); RatchetingSession.initializeSession(sessionRecord.getSessionState(), preKey.getSignedPreKey() == null ? 2 : 3, - ourBaseKey, theirBaseKey, ourEphemeralKey, - theirEphemeralKey, ourPreKey, theirPreKey, - ourIdentityKey, theirIdentityKey); + parameters.create()); sessionRecord.getSessionState().setPendingPreKey(preKey.getPreKeyId(), preKey.getSignedPreKeyId(), ourBaseKey.getPublicKey()); sessionRecord.getSessionState().setLocalRegistrationId(identityKeyStore.getLocalRegistrationId()); @@ -283,11 +282,9 @@ public class SessionBuilder { } private KeyExchangeMessage processInitiate(KeyExchangeMessage message) throws InvalidKeyException { - ECKeyPair ourBaseKey, ourEphemeralKey; - IdentityKeyPair ourIdentityKey; - - int flags = KeyExchangeMessage.RESPONSE_FLAG; - SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId); + InitializationParameters.Builder parameters = InitializationParameters.newBuilder(); + int flags = KeyExchangeMessage.RESPONSE_FLAG; + SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId); if (message.getVersion() >= 3 && !Curve.verifySignature(message.getIdentityKey().getPublicKey(), @@ -299,32 +296,38 @@ public class SessionBuilder { if (!sessionRecord.getSessionState().hasPendingKeyExchange()) { Log.w(TAG, "We don't have a pending initiate..."); - ourBaseKey = Curve.generateKeyPair(true); - ourEphemeralKey = Curve.generateKeyPair(true); - ourIdentityKey = identityKeyStore.getIdentityKeyPair(); + parameters.setOurBaseKey(Curve.generateKeyPair(true)); + parameters.setOurEphemeralKey(Curve.generateKeyPair(true)); + parameters.setOurIdentityKey(identityKeyStore.getIdentityKeyPair()); + parameters.setOurPreKey(Optional.absent()); } else { Log.w(TAG, "We already have a pending initiate, responding as simultaneous initiate..."); - ourBaseKey = sessionRecord.getSessionState().getPendingKeyExchangeBaseKey(); - ourEphemeralKey = sessionRecord.getSessionState().getPendingKeyExchangeEphemeralKey(); - ourIdentityKey = sessionRecord.getSessionState().getPendingKeyExchangeIdentityKey(); + parameters.setOurBaseKey(sessionRecord.getSessionState().getPendingKeyExchangeBaseKey()); + parameters.setOurEphemeralKey(sessionRecord.getSessionState().getPendingKeyExchangeEphemeralKey()); + parameters.setOurIdentityKey(sessionRecord.getSessionState().getPendingKeyExchangeIdentityKey()); + parameters.setOurPreKey(Optional.absent()); flags |= KeyExchangeMessage.SIMULTAENOUS_INITIATE_FLAG; } + parameters.setTheirBaseKey(message.getBaseKey()); + parameters.setTheirEphemeralKey(message.getEphemeralKey()); + parameters.setTheirPreKey(Optional.absent()); + parameters.setTheirIdentityKey(message.getIdentityKey()); + sessionRecord.reset(); RatchetingSession.initializeSession(sessionRecord.getSessionState(), Math.min(message.getMaxVersion(), CiphertextMessage.CURRENT_VERSION), - ourBaseKey, message.getBaseKey(), - ourEphemeralKey, message.getEphemeralKey(), - ourBaseKey, message.getBaseKey(), - ourIdentityKey, message.getIdentityKey()); + parameters.create()); sessionStore.storeSession(recipientId, deviceId, sessionRecord); identityKeyStore.saveIdentity(recipientId, message.getIdentityKey()); return new KeyExchangeMessage(sessionRecord.getSessionState().getSessionVersion(), - message.getSequence(), flags, ourBaseKey.getPublicKey(), null, - ourEphemeralKey.getPublicKey(), ourIdentityKey.getPublicKey(), + message.getSequence(), flags, + parameters.getOurBaseKey().getPublicKey(), null, + parameters.getOurEphemeralKey().getPublicKey(), + parameters.getOurIdentityKey().getPublicKey(), sessionRecord.getSessionState().getVerification()); } @@ -342,18 +345,23 @@ public class SessionBuilder { else return; } - ECKeyPair ourBaseKey = sessionRecord.getSessionState().getPendingKeyExchangeBaseKey(); - ECKeyPair ourEphemeralKey = sessionRecord.getSessionState().getPendingKeyExchangeEphemeralKey(); - IdentityKeyPair ourIdentityKey = sessionRecord.getSessionState().getPendingKeyExchangeIdentityKey(); + InitializationParameters parameters = + InitializationParameters.newBuilder() + .setOurBaseKey(sessionRecord.getSessionState().getPendingKeyExchangeBaseKey()) + .setOurEphemeralKey(sessionRecord.getSessionState().getPendingKeyExchangeEphemeralKey()) + .setOurPreKey(Optional.absent()) + .setOurIdentityKey(sessionRecord.getSessionState().getPendingKeyExchangeIdentityKey()) + .setTheirBaseKey(message.getBaseKey()) + .setTheirEphemeralKey(message.getEphemeralKey()) + .setTheirPreKey(Optional.absent()) + .setTheirIdentityKey(message.getIdentityKey()) + .create(); sessionRecord.reset(); RatchetingSession.initializeSession(sessionRecord.getSessionState(), Math.min(message.getMaxVersion(), CiphertextMessage.CURRENT_VERSION), - ourBaseKey, message.getBaseKey(), - ourEphemeralKey, message.getEphemeralKey(), - ourBaseKey, message.getBaseKey(), - ourIdentityKey, message.getIdentityKey()); + parameters); if (sessionRecord.getSessionState().getSessionVersion() >= 3 && !MessageDigest.isEqual(message.getVerificationTag(), diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/ratchet/RatchetingSession.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/ratchet/RatchetingSession.java index a314791783..7b33088ed3 100644 --- a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/ratchet/RatchetingSession.java +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/ratchet/RatchetingSession.java @@ -26,6 +26,7 @@ import org.whispersystems.libaxolotl.kdf.HKDF; import org.whispersystems.libaxolotl.state.SessionState; import org.whispersystems.libaxolotl.util.ByteUtil; import org.whispersystems.libaxolotl.util.Pair; +import org.whispersystems.libaxolotl.util.guava.Optional; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -34,97 +35,58 @@ import java.util.Arrays; public class RatchetingSession { - public static void initializeSession(SessionState sessionState, - int sessionVersion, - ECKeyPair ourBaseKey, - ECPublicKey theirBaseKey, - ECKeyPair ourEphemeralKey, - ECPublicKey theirEphemeralKey, - ECKeyPair ourPreKey, - ECPublicKey theirPreKey, - IdentityKeyPair ourIdentityKey, - IdentityKey theirIdentityKey) + public static void initializeSession(SessionState sessionState, + int sessionVersion, + InitializationParameters parameters) throws InvalidKeyException { - if (isAlice(ourBaseKey.getPublicKey(), theirBaseKey, ourEphemeralKey.getPublicKey(), theirEphemeralKey)) { - initializeSessionAsAlice(sessionState, sessionVersion, ourBaseKey, theirBaseKey, theirEphemeralKey, - ourPreKey, theirPreKey, ourIdentityKey, theirIdentityKey); - } else { - initializeSessionAsBob(sessionState, sessionVersion, ourBaseKey, theirBaseKey, ourEphemeralKey, - ourPreKey, theirPreKey, ourIdentityKey, theirIdentityKey); - } + if (isAlice(parameters)) initializeSessionAsAlice(sessionState, sessionVersion, parameters); + else initializeSessionAsBob(sessionState, sessionVersion, parameters); sessionState.setSessionVersion(sessionVersion); } private static void initializeSessionAsAlice(SessionState sessionState, int sessionVersion, - ECKeyPair ourBaseKey, ECPublicKey theirBaseKey, - ECPublicKey theirEphemeralKey, - ECKeyPair ourPreKey, ECPublicKey theirPreKey, - IdentityKeyPair ourIdentityKey, - IdentityKey theirIdentityKey) + InitializationParameters parameters) throws InvalidKeyException { - sessionState.setRemoteIdentityKey(theirIdentityKey); - sessionState.setLocalIdentityKey(ourIdentityKey.getPublicKey()); + sessionState.setRemoteIdentityKey(parameters.getTheirIdentityKey()); + sessionState.setLocalIdentityKey(parameters.getOurIdentityKey().getPublicKey()); ECKeyPair sendingKey = Curve.generateKeyPair(true); - DHEResult result = calculate4DHE(true, sessionVersion, ourBaseKey, theirBaseKey, - ourPreKey, theirPreKey, ourIdentityKey, theirIdentityKey); + DHEResult result = calculate4DHE(true, sessionVersion, parameters); - Pair sendingChain = result.getRootKey().createChain(theirEphemeralKey, sendingKey); + Pair sendingChain = result.getRootKey().createChain(parameters.getTheirEphemeralKey(), sendingKey); - sessionState.addReceiverChain(theirEphemeralKey, result.getChainKey()); + sessionState.addReceiverChain(parameters.getTheirEphemeralKey(), result.getChainKey()); sessionState.setSenderChain(sendingKey, sendingChain.second()); sessionState.setRootKey(sendingChain.first()); if (sessionVersion >= 3) { - VerifyKey verifyKey = result.getVerifyKey(); - byte[] verificationTag = verifyKey.generateVerification(ourBaseKey.getPublicKey(), - ourPreKey.getPublicKey(), - ourIdentityKey.getPublicKey().getPublicKey(), - theirBaseKey, theirPreKey, - theirIdentityKey.getPublicKey()); - - sessionState.setVerification(verificationTag); + sessionState.setVerification(calculateVerificationTag(true, result.getVerifyKey(), parameters)); } } private static void initializeSessionAsBob(SessionState sessionState, int sessionVersion, - ECKeyPair ourBaseKey, ECPublicKey theirBaseKey, - ECKeyPair ourEphemeralKey, - ECKeyPair ourPreKey, ECPublicKey theirPreKey, - IdentityKeyPair ourIdentityKey, - IdentityKey theirIdentityKey) + InitializationParameters parameters) throws InvalidKeyException { - sessionState.setRemoteIdentityKey(theirIdentityKey); - sessionState.setLocalIdentityKey(ourIdentityKey.getPublicKey()); + sessionState.setRemoteIdentityKey(parameters.getTheirIdentityKey()); + sessionState.setLocalIdentityKey(parameters.getOurIdentityKey().getPublicKey()); - DHEResult result = calculate4DHE(false, sessionVersion, ourBaseKey, theirBaseKey, - ourPreKey, theirPreKey, ourIdentityKey, theirIdentityKey); + DHEResult result = calculate4DHE(false, sessionVersion, parameters); - sessionState.setSenderChain(ourEphemeralKey, result.getChainKey()); + sessionState.setSenderChain(parameters.getOurEphemeralKey(), result.getChainKey()); sessionState.setRootKey(result.getRootKey()); if (sessionVersion >= 3) { - VerifyKey verifyKey = result.getVerifyKey(); - byte[] verificationTag = verifyKey.generateVerification(theirBaseKey, theirPreKey, - theirIdentityKey.getPublicKey(), - ourBaseKey.getPublicKey(), - ourPreKey.getPublicKey(), - ourIdentityKey.getPublicKey().getPublicKey()); - - sessionState.setVerification(verificationTag); + sessionState.setVerification(calculateVerificationTag(false, result.getVerifyKey(), parameters)); } } - private static DHEResult calculate4DHE(boolean isAlice, int sessionVersion, - ECKeyPair ourBaseKey, ECPublicKey theirBaseKey, - ECKeyPair ourPreKey, ECPublicKey theirPreKey, - IdentityKeyPair ourIdentity, IdentityKey theirIdentity) + private static DHEResult calculate4DHE(boolean isAlice, int sessionVersion, InitializationParameters parameters) throws InvalidKeyException { try { @@ -138,17 +100,22 @@ public class RatchetingSession { } if (isAlice) { - secrets.write(Curve.calculateAgreement(theirBaseKey, ourIdentity.getPrivateKey())); - secrets.write(Curve.calculateAgreement(theirIdentity.getPublicKey(), ourBaseKey.getPrivateKey())); + secrets.write(Curve.calculateAgreement(parameters.getTheirBaseKey(), + parameters.getOurIdentityKey().getPrivateKey())); + secrets.write(Curve.calculateAgreement(parameters.getTheirIdentityKey().getPublicKey(), + parameters.getOurBaseKey().getPrivateKey())); } else { - secrets.write(Curve.calculateAgreement(theirIdentity.getPublicKey(), ourBaseKey.getPrivateKey())); - secrets.write(Curve.calculateAgreement(theirBaseKey, ourIdentity.getPrivateKey())); + secrets.write(Curve.calculateAgreement(parameters.getTheirIdentityKey().getPublicKey(), + parameters.getOurBaseKey().getPrivateKey())); + secrets.write(Curve.calculateAgreement(parameters.getTheirBaseKey(), + parameters.getOurIdentityKey().getPrivateKey())); } - secrets.write(Curve.calculateAgreement(theirBaseKey, ourBaseKey.getPrivateKey())); + secrets.write(Curve.calculateAgreement(parameters.getTheirBaseKey(), + parameters.getOurBaseKey().getPrivateKey())); - if (sessionVersion >= 3) { - secrets.write(Curve.calculateAgreement(theirPreKey, ourPreKey.getPrivateKey())); + if (sessionVersion >= 3 && parameters.getTheirPreKey().isPresent() && parameters.getOurPreKey().isPresent()) { + secrets.write(Curve.calculateAgreement(parameters.getTheirPreKey().get(), parameters.getOurPreKey().get().getPrivateKey())); } byte[] derivedSecretBytes = kdf.deriveSecrets(secrets.toByteArray(), "WhisperText".getBytes(), 96); @@ -163,24 +130,191 @@ public class RatchetingSession { } } - private static boolean isAlice(ECPublicKey ourBaseKey, ECPublicKey theirBaseKey, - ECPublicKey ourEphemeralKey, ECPublicKey theirEphemeralKey) + private static byte[] calculateVerificationTag(boolean isAlice, VerifyKey verifyKey, + InitializationParameters parameters) { - if (ourEphemeralKey.equals(ourBaseKey)) { + if (isAlice) { + return verifyKey.generateVerification(parameters.getOurBaseKey().getPublicKey(), + getPublicKey(parameters.getOurPreKey()), + parameters.getOurIdentityKey().getPublicKey().getPublicKey(), + parameters.getTheirBaseKey(), + parameters.getTheirPreKey(), + parameters.getTheirIdentityKey().getPublicKey()); + } else { + return verifyKey.generateVerification(parameters.getTheirBaseKey(), + parameters.getTheirPreKey(), + parameters.getTheirIdentityKey().getPublicKey(), + parameters.getOurBaseKey().getPublicKey(), + getPublicKey(parameters.getOurPreKey()), + parameters.getOurIdentityKey().getPublicKey().getPublicKey()); + } + } + + private static boolean isAlice(InitializationParameters parameters) + { + if (parameters.getOurEphemeralKey().equals(parameters.getOurBaseKey())) { return false; } - if (theirEphemeralKey.equals(theirBaseKey)) { + if (parameters.getTheirEphemeralKey().equals(parameters.getTheirBaseKey())) { return true; } - return isLowEnd(ourBaseKey, theirBaseKey); + return isLowEnd(parameters.getOurBaseKey().getPublicKey(), parameters.getTheirBaseKey()); } private static boolean isLowEnd(ECPublicKey ourKey, ECPublicKey theirKey) { return ourKey.compareTo(theirKey) < 0; } + private static Optional getPublicKey(Optional keyPair) { + if (keyPair.isPresent()) return Optional.of(keyPair.get().getPublicKey()); + else return Optional.absent(); + } + + public static class InitializationParameters { + private final ECKeyPair ourBaseKey; + private final ECKeyPair ourEphemeralKey; + private final Optional ourPreKey; + private final IdentityKeyPair ourIdentityKey; + + private final ECPublicKey theirBaseKey; + private final ECPublicKey theirEphemeralKey; + private final Optional theirPreKey; + private final IdentityKey theirIdentityKey; + + public InitializationParameters(ECKeyPair ourBaseKey, ECKeyPair ourEphemeralKey, + Optional ourPreKey, IdentityKeyPair ourIdentityKey, + ECPublicKey theirBaseKey, ECPublicKey theirEphemeralKey, + Optional theirPreKey, IdentityKey theirIdentityKey) + { + this.ourBaseKey = ourBaseKey; + this.ourEphemeralKey = ourEphemeralKey; + this.ourPreKey = ourPreKey; + this.ourIdentityKey = ourIdentityKey; + this.theirBaseKey = theirBaseKey; + this.theirEphemeralKey = theirEphemeralKey; + this.theirPreKey = theirPreKey; + this.theirIdentityKey = theirIdentityKey; + } + + public ECKeyPair getOurBaseKey() { + return ourBaseKey; + } + + public ECKeyPair getOurEphemeralKey() { + return ourEphemeralKey; + } + + public Optional getOurPreKey() { + return ourPreKey; + } + + public IdentityKeyPair getOurIdentityKey() { + return ourIdentityKey; + } + + public ECPublicKey getTheirBaseKey() { + return theirBaseKey; + } + + public ECPublicKey getTheirEphemeralKey() { + return theirEphemeralKey; + } + + public Optional getTheirPreKey() { + return theirPreKey; + } + + public IdentityKey getTheirIdentityKey() { + return theirIdentityKey; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + private ECKeyPair ourBaseKey; + private ECKeyPair ourEphemeralKey; + private Optional ourPreKey; + private IdentityKeyPair ourIdentityKey; + private ECPublicKey theirBaseKey; + private ECPublicKey theirEphemeralKey; + private Optional theirPreKey; + private IdentityKey theirIdentityKey; + + public Builder setOurBaseKey(ECKeyPair ourBaseKey) { + this.ourBaseKey = ourBaseKey; + return this; + } + + public ECKeyPair getOurBaseKey() { + return ourBaseKey; + } + + public Builder setOurEphemeralKey(ECKeyPair ourEphemeralKey) { + this.ourEphemeralKey = ourEphemeralKey; + return this; + } + + public ECKeyPair getOurEphemeralKey() { + return ourEphemeralKey; + } + + public Builder setOurPreKey(Optional ourPreKey) { + this.ourPreKey = ourPreKey; + return this; + } + + public Builder setOurIdentityKey(IdentityKeyPair ourIdentityKey) { + this.ourIdentityKey = ourIdentityKey; + return this; + } + + public IdentityKeyPair getOurIdentityKey() { + return ourIdentityKey; + } + + public Builder setTheirBaseKey(ECPublicKey theirBaseKey) { + this.theirBaseKey = theirBaseKey; + return this; + } + + public ECPublicKey getTheirBaseKey() { + return theirBaseKey; + } + + public Builder setTheirEphemeralKey(ECPublicKey theirEphemeralKey) { + this.theirEphemeralKey = theirEphemeralKey; + return this; + } + + public Builder setTheirPreKey(Optional theirPreKey) { + this.theirPreKey = theirPreKey; + return this; + } + + public Builder setTheirIdentityKey(IdentityKey theirIdentityKey) { + this.theirIdentityKey = theirIdentityKey; + return this; + } + + public RatchetingSession.InitializationParameters create() { + if (ourBaseKey == null || ourEphemeralKey == null || ourPreKey == null || ourIdentityKey == null || + theirBaseKey == null || theirEphemeralKey == null || theirPreKey == null || theirIdentityKey == null) + { + throw new IllegalArgumentException("All parameters not specified!"); + } + + return new RatchetingSession.InitializationParameters(ourBaseKey, ourEphemeralKey, + ourPreKey, ourIdentityKey, + theirBaseKey, theirEphemeralKey, + theirPreKey, theirIdentityKey); + } + } + } + private static class DHEResult { private final RootKey rootKey; private final ChainKey chainKey; diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/ratchet/VerifyKey.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/ratchet/VerifyKey.java index 8f9c2e2d6d..19294378c7 100644 --- a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/ratchet/VerifyKey.java +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/ratchet/VerifyKey.java @@ -2,6 +2,7 @@ package org.whispersystems.libaxolotl.ratchet; import org.whispersystems.libaxolotl.ecc.ECPublicKey; import org.whispersystems.libaxolotl.util.ByteUtil; +import org.whispersystems.libaxolotl.util.guava.Optional; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -23,8 +24,12 @@ public class VerifyKey { return key; } - public byte[] generateVerification(ECPublicKey aliceBaseKey, ECPublicKey alicePreKey, ECPublicKey aliceIdentityKey, - ECPublicKey bobBaseKey, ECPublicKey bobPreKey, ECPublicKey bobIdentityKey) + public byte[] generateVerification(ECPublicKey aliceBaseKey, + Optional alicePreKey, + ECPublicKey aliceIdentityKey, + ECPublicKey bobBaseKey, + Optional bobPreKey, + ECPublicKey bobIdentityKey) { try { Mac mac = Mac.getInstance("HmacSHA256"); @@ -32,15 +37,15 @@ public class VerifyKey { mac.update(VERIFICATION_INFO); mac.update(aliceBaseKey.serialize()); - mac.update(alicePreKey.serialize()); mac.update(aliceIdentityKey.serialize()); mac.update(bobBaseKey.serialize()); + mac.update(bobIdentityKey.serialize()); - if (bobPreKey != null) { - mac.update(bobPreKey.serialize()); + if (alicePreKey.isPresent() && bobPreKey.isPresent()) { + mac.update(alicePreKey.get().serialize()); + mac.update(bobPreKey.get().serialize()); } - mac.update(bobIdentityKey.serialize()); return ByteUtil.trim(mac.doFinal(), 8); } catch (NoSuchAlgorithmException | InvalidKeyException e) { diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Absent.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Absent.java new file mode 100644 index 0000000000..bd06ded966 --- /dev/null +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Absent.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2011 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.whispersystems.libaxolotl.util.guava; + +import static org.whispersystems.libaxolotl.util.guava.Preconditions.checkNotNull; + + + +import java.util.Collections; +import java.util.Set; + + +/** + * Implementation of an {@link Optional} not containing a reference. + */ + +final class Absent extends Optional { + static final Absent INSTANCE = new Absent(); + + @Override public boolean isPresent() { + return false; + } + + @Override public Object get() { + throw new IllegalStateException("value is absent"); + } + + @Override public Object or(Object defaultValue) { + return checkNotNull(defaultValue, "use orNull() instead of or(null)"); + } + + @SuppressWarnings("unchecked") // safe covariant cast + @Override public Optional or(Optional secondChoice) { + return (Optional) checkNotNull(secondChoice); + } + + @Override public Object or(Supplier supplier) { + return checkNotNull(supplier.get(), + "use orNull() instead of a Supplier that returns null"); + } + + @Override public Object orNull() { + return null; + } + + @Override public Set asSet() { + return Collections.emptySet(); + } + + @Override + public Optional transform(Function function) { + checkNotNull(function); + return Optional.absent(); + } + + @Override public boolean equals(Object object) { + return object == this; + } + + @Override public int hashCode() { + return 0x598df91c; + } + + @Override public String toString() { + return "Optional.absent()"; + } + + private Object readResolve() { + return INSTANCE; + } + + private static final long serialVersionUID = 0; +} diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Function.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Function.java new file mode 100644 index 0000000000..1ad516c5fd --- /dev/null +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Function.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.whispersystems.libaxolotl.util.guava; + + + +/** + * Determines an output value based on an input value. + * + *

See the Guava User Guide article on the use of {@code + * Function}. + * + * @author Kevin Bourrillion + * @since 2.0 (imported from Google Collections Library) + */ + +public interface Function { + /** + * Returns the result of applying this function to {@code input}. This method is generally + * expected, but not absolutely required, to have the following properties: + * + *

    + *
  • Its execution does not cause any observable side effects. + *
  • The computation is consistent with equals; that is, {@link Objects#equal + * Objects.equal}{@code (a, b)} implies that {@code Objects.equal(function.apply(a), + * function.apply(b))}. + *
+ * + * @throws NullPointerException if {@code input} is null and this function does not accept null + * arguments + */ + T apply(F input); + + /** + * Indicates whether another object is equal to this function. + * + *

Most implementations will have no reason to override the behavior of {@link Object#equals}. + * However, an implementation may also choose to return {@code true} whenever {@code object} is a + * {@link Function} that it considers interchangeable with this one. "Interchangeable" + * typically means that {@code Objects.equal(this.apply(f), that.apply(f))} is true for all + * {@code f} of type {@code F}. Note that a {@code false} result from this method does not imply + * that the functions are known not to be interchangeable. + */ + @Override + boolean equals(Object object); +} diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Optional.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Optional.java new file mode 100644 index 0000000000..4f2de832aa --- /dev/null +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Optional.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2011 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.whispersystems.libaxolotl.util.guava; + +import static org.whispersystems.libaxolotl.util.guava.Preconditions.checkNotNull; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.Set; + + +/** + * An immutable object that may contain a non-null reference to another object. Each + * instance of this type either contains a non-null reference, or contains nothing (in + * which case we say that the reference is "absent"); it is never said to "contain {@code + * null}". + * + *

A non-null {@code Optional} reference can be used as a replacement for a nullable + * {@code T} reference. It allows you to represent "a {@code T} that must be present" and + * a "a {@code T} that might be absent" as two distinct types in your program, which can + * aid clarity. + * + *

Some uses of this class include + * + *

    + *
  • As a method return type, as an alternative to returning {@code null} to indicate + * that no value was available + *
  • To distinguish between "unknown" (for example, not present in a map) and "known to + * have no value" (present in the map, with value {@code Optional.absent()}) + *
  • To wrap nullable references for storage in a collection that does not support + * {@code null} (though there are + * + * several other approaches to this that should be considered first) + *
+ * + *

A common alternative to using this class is to find or create a suitable + * null object for the + * type in question. + * + *

This class is not intended as a direct analogue of any existing "option" or "maybe" + * construct from other programming environments, though it may bear some similarities. + * + *

See the Guava User Guide article on + * using {@code Optional}. + * + * @param the type of instance that can be contained. {@code Optional} is naturally + * covariant on this type, so it is safe to cast an {@code Optional} to {@code + * Optional} for any supertype {@code S} of {@code T}. + * @author Kurt Alfred Kluever + * @author Kevin Bourrillion + * @since 10.0 + */ +public abstract class Optional implements Serializable { + /** + * Returns an {@code Optional} instance with no contained reference. + */ + @SuppressWarnings("unchecked") + public static Optional absent() { + return (Optional) Absent.INSTANCE; + } + + /** + * Returns an {@code Optional} instance containing the given non-null reference. + */ + public static Optional of(T reference) { + return new Present(checkNotNull(reference)); + } + + /** + * If {@code nullableReference} is non-null, returns an {@code Optional} instance containing that + * reference; otherwise returns {@link Optional#absent}. + */ + public static Optional fromNullable(T nullableReference) { + return (nullableReference == null) + ? Optional.absent() + : new Present(nullableReference); + } + + Optional() {} + + /** + * Returns {@code true} if this holder contains a (non-null) instance. + */ + public abstract boolean isPresent(); + + /** + * Returns the contained instance, which must be present. If the instance might be + * absent, use {@link #or(Object)} or {@link #orNull} instead. + * + * @throws IllegalStateException if the instance is absent ({@link #isPresent} returns + * {@code false}) + */ + public abstract T get(); + + /** + * Returns the contained instance if it is present; {@code defaultValue} otherwise. If + * no default value should be required because the instance is known to be present, use + * {@link #get()} instead. For a default value of {@code null}, use {@link #orNull}. + * + *

Note about generics: The signature {@code public T or(T defaultValue)} is overly + * restrictive. However, the ideal signature, {@code public S or(S)}, is not legal + * Java. As a result, some sensible operations involving subtypes are compile errors: + *

   {@code
+   *
+   *   Optional optionalInt = getSomeOptionalInt();
+   *   Number value = optionalInt.or(0.5); // error
+   *
+   *   FluentIterable numbers = getSomeNumbers();
+   *   Optional first = numbers.first();
+   *   Number value = first.or(0.5); // error}
+ * + * As a workaround, it is always safe to cast an {@code Optional} to {@code + * Optional}. Casting either of the above example {@code Optional} instances to {@code + * Optional} (where {@code Number} is the desired output type) solves the problem: + *
   {@code
+   *
+   *   Optional optionalInt = (Optional) getSomeOptionalInt();
+   *   Number value = optionalInt.or(0.5); // fine
+   *
+   *   FluentIterable numbers = getSomeNumbers();
+   *   Optional first = (Optional) numbers.first();
+   *   Number value = first.or(0.5); // fine}
+ */ + public abstract T or(T defaultValue); + + /** + * Returns this {@code Optional} if it has a value present; {@code secondChoice} + * otherwise. + */ + public abstract Optional or(Optional secondChoice); + + /** + * Returns the contained instance if it is present; {@code supplier.get()} otherwise. If the + * supplier returns {@code null}, a {@link NullPointerException} is thrown. + * + * @throws NullPointerException if the supplier returns {@code null} + */ + public abstract T or(Supplier supplier); + + /** + * Returns the contained instance if it is present; {@code null} otherwise. If the + * instance is known to be present, use {@link #get()} instead. + */ + public abstract T orNull(); + + /** + * Returns an immutable singleton {@link Set} whose only element is the contained instance + * if it is present; an empty immutable {@link Set} otherwise. + * + * @since 11.0 + */ + public abstract Set asSet(); + + /** + * If the instance is present, it is transformed with the given {@link Function}; otherwise, + * {@link Optional#absent} is returned. If the function returns {@code null}, a + * {@link NullPointerException} is thrown. + * + * @throws NullPointerException if the function returns {@code null} + * + * @since 12.0 + */ + + public abstract Optional transform(Function function); + + /** + * Returns {@code true} if {@code object} is an {@code Optional} instance, and either + * the contained references are {@linkplain Object#equals equal} to each other or both + * are absent. Note that {@code Optional} instances of differing parameterized types can + * be equal. + */ + @Override public abstract boolean equals(Object object); + + /** + * Returns a hash code for this instance. + */ + @Override public abstract int hashCode(); + + /** + * Returns a string representation for this instance. The form of this string + * representation is unspecified. + */ + @Override public abstract String toString(); + + /** + * Returns the value of each present instance from the supplied {@code optionals}, in order, + * skipping over occurrences of {@link Optional#absent}. Iterators are unmodifiable and are + * evaluated lazily. + * + * @since 11.0 (generics widened in 13.0) + */ + +// public static Iterable presentInstances( +// final Iterable> optionals) { +// checkNotNull(optionals); +// return new Iterable() { +// @Override public Iterator iterator() { +// return new AbstractIterator() { +// private final Iterator> iterator = +// checkNotNull(optionals.iterator()); +// +// @Override protected T computeNext() { +// while (iterator.hasNext()) { +// Optional optional = iterator.next(); +// if (optional.isPresent()) { +// return optional.get(); +// } +// } +// return endOfData(); +// } +// }; +// }; +// }; +// } + + private static final long serialVersionUID = 0; +} diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Preconditions.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Preconditions.java new file mode 100644 index 0000000000..ce253c65f0 --- /dev/null +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Preconditions.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.whispersystems.libaxolotl.util.guava; + + +import java.util.NoSuchElementException; + + + +/** + * Simple static methods to be called at the start of your own methods to verify + * correct arguments and state. This allows constructs such as + *
+ *     if (count <= 0) {
+ *       throw new IllegalArgumentException("must be positive: " + count);
+ *     }
+ * + * to be replaced with the more compact + *
+ *     checkArgument(count > 0, "must be positive: %s", count);
+ * + * Note that the sense of the expression is inverted; with {@code Preconditions} + * you declare what you expect to be true, just as you do with an + * + * {@code assert} or a JUnit {@code assertTrue} call. + * + *

Warning: only the {@code "%s"} specifier is recognized as a + * placeholder in these messages, not the full range of {@link + * String#format(String, Object[])} specifiers. + * + *

Take care not to confuse precondition checking with other similar types + * of checks! Precondition exceptions -- including those provided here, but also + * {@link IndexOutOfBoundsException}, {@link NoSuchElementException}, {@link + * UnsupportedOperationException} and others -- are used to signal that the + * calling method has made an error. This tells the caller that it should + * not have invoked the method when it did, with the arguments it did, or + * perhaps ever. Postcondition or other invariant failures should not throw + * these types of exceptions. + * + *

See the Guava User Guide on + * using {@code Preconditions}. + * + * @author Kevin Bourrillion + * @since 2.0 (imported from Google Collections Library) + */ + +public final class Preconditions { + private Preconditions() {} + + /** + * Ensures the truth of an expression involving one or more parameters to the + * calling method. + * + * @param expression a boolean expression + * @throws IllegalArgumentException if {@code expression} is false + */ + public static void checkArgument(boolean expression) { + if (!expression) { + throw new IllegalArgumentException(); + } + } + + /** + * Ensures the truth of an expression involving one or more parameters to the + * calling method. + * + * @param expression a boolean expression + * @param errorMessage the exception message to use if the check fails; will + * be converted to a string using {@link String#valueOf(Object)} + * @throws IllegalArgumentException if {@code expression} is false + */ + public static void checkArgument( + boolean expression, Object errorMessage) { + if (!expression) { + throw new IllegalArgumentException(String.valueOf(errorMessage)); + } + } + + /** + * Ensures the truth of an expression involving one or more parameters to the + * calling method. + * + * @param expression a boolean expression + * @param errorMessageTemplate a template for the exception message should the + * check fail. The message is formed by replacing each {@code %s} + * placeholder in the template with an argument. These are matched by + * position - the first {@code %s} gets {@code errorMessageArgs[0]}, etc. + * Unmatched arguments will be appended to the formatted message in square + * braces. Unmatched placeholders will be left as-is. + * @param errorMessageArgs the arguments to be substituted into the message + * template. Arguments are converted to strings using + * {@link String#valueOf(Object)}. + * @throws IllegalArgumentException if {@code expression} is false + * @throws NullPointerException if the check fails and either {@code + * errorMessageTemplate} or {@code errorMessageArgs} is null (don't let + * this happen) + */ + public static void checkArgument(boolean expression, + String errorMessageTemplate, + Object... errorMessageArgs) { + if (!expression) { + throw new IllegalArgumentException( + format(errorMessageTemplate, errorMessageArgs)); + } + } + + /** + * Ensures the truth of an expression involving the state of the calling + * instance, but not involving any parameters to the calling method. + * + * @param expression a boolean expression + * @throws IllegalStateException if {@code expression} is false + */ + public static void checkState(boolean expression) { + if (!expression) { + throw new IllegalStateException(); + } + } + + /** + * Ensures the truth of an expression involving the state of the calling + * instance, but not involving any parameters to the calling method. + * + * @param expression a boolean expression + * @param errorMessage the exception message to use if the check fails; will + * be converted to a string using {@link String#valueOf(Object)} + * @throws IllegalStateException if {@code expression} is false + */ + public static void checkState( + boolean expression, Object errorMessage) { + if (!expression) { + throw new IllegalStateException(String.valueOf(errorMessage)); + } + } + + /** + * Ensures the truth of an expression involving the state of the calling + * instance, but not involving any parameters to the calling method. + * + * @param expression a boolean expression + * @param errorMessageTemplate a template for the exception message should the + * check fail. The message is formed by replacing each {@code %s} + * placeholder in the template with an argument. These are matched by + * position - the first {@code %s} gets {@code errorMessageArgs[0]}, etc. + * Unmatched arguments will be appended to the formatted message in square + * braces. Unmatched placeholders will be left as-is. + * @param errorMessageArgs the arguments to be substituted into the message + * template. Arguments are converted to strings using + * {@link String#valueOf(Object)}. + * @throws IllegalStateException if {@code expression} is false + * @throws NullPointerException if the check fails and either {@code + * errorMessageTemplate} or {@code errorMessageArgs} is null (don't let + * this happen) + */ + public static void checkState(boolean expression, + String errorMessageTemplate, + Object... errorMessageArgs) { + if (!expression) { + throw new IllegalStateException( + format(errorMessageTemplate, errorMessageArgs)); + } + } + + /** + * Ensures that an object reference passed as a parameter to the calling + * method is not null. + * + * @param reference an object reference + * @return the non-null reference that was validated + * @throws NullPointerException if {@code reference} is null + */ + public static T checkNotNull(T reference) { + if (reference == null) { + throw new NullPointerException(); + } + return reference; + } + + /** + * Ensures that an object reference passed as a parameter to the calling + * method is not null. + * + * @param reference an object reference + * @param errorMessage the exception message to use if the check fails; will + * be converted to a string using {@link String#valueOf(Object)} + * @return the non-null reference that was validated + * @throws NullPointerException if {@code reference} is null + */ + public static T checkNotNull(T reference, Object errorMessage) { + if (reference == null) { + throw new NullPointerException(String.valueOf(errorMessage)); + } + return reference; + } + + /** + * Ensures that an object reference passed as a parameter to the calling + * method is not null. + * + * @param reference an object reference + * @param errorMessageTemplate a template for the exception message should the + * check fail. The message is formed by replacing each {@code %s} + * placeholder in the template with an argument. These are matched by + * position - the first {@code %s} gets {@code errorMessageArgs[0]}, etc. + * Unmatched arguments will be appended to the formatted message in square + * braces. Unmatched placeholders will be left as-is. + * @param errorMessageArgs the arguments to be substituted into the message + * template. Arguments are converted to strings using + * {@link String#valueOf(Object)}. + * @return the non-null reference that was validated + * @throws NullPointerException if {@code reference} is null + */ + public static T checkNotNull(T reference, + String errorMessageTemplate, + Object... errorMessageArgs) { + if (reference == null) { + // If either of these parameters is null, the right thing happens anyway + throw new NullPointerException( + format(errorMessageTemplate, errorMessageArgs)); + } + return reference; + } + + /* + * All recent hotspots (as of 2009) *really* like to have the natural code + * + * if (guardExpression) { + * throw new BadException(messageExpression); + * } + * + * refactored so that messageExpression is moved to a separate + * String-returning method. + * + * if (guardExpression) { + * throw new BadException(badMsg(...)); + * } + * + * The alternative natural refactorings into void or Exception-returning + * methods are much slower. This is a big deal - we're talking factors of + * 2-8 in microbenchmarks, not just 10-20%. (This is a hotspot optimizer + * bug, which should be fixed, but that's a separate, big project). + * + * The coding pattern above is heavily used in java.util, e.g. in ArrayList. + * There is a RangeCheckMicroBenchmark in the JDK that was used to test this. + * + * But the methods in this class want to throw different exceptions, + * depending on the args, so it appears that this pattern is not directly + * applicable. But we can use the ridiculous, devious trick of throwing an + * exception in the middle of the construction of another exception. + * Hotspot is fine with that. + */ + + /** + * Ensures that {@code index} specifies a valid element in an array, + * list or string of size {@code size}. An element index may range from zero, + * inclusive, to {@code size}, exclusive. + * + * @param index a user-supplied index identifying an element of an array, list + * or string + * @param size the size of that array, list or string + * @return the value of {@code index} + * @throws IndexOutOfBoundsException if {@code index} is negative or is not + * less than {@code size} + * @throws IllegalArgumentException if {@code size} is negative + */ + public static int checkElementIndex(int index, int size) { + return checkElementIndex(index, size, "index"); + } + + /** + * Ensures that {@code index} specifies a valid element in an array, + * list or string of size {@code size}. An element index may range from zero, + * inclusive, to {@code size}, exclusive. + * + * @param index a user-supplied index identifying an element of an array, list + * or string + * @param size the size of that array, list or string + * @param desc the text to use to describe this index in an error message + * @return the value of {@code index} + * @throws IndexOutOfBoundsException if {@code index} is negative or is not + * less than {@code size} + * @throws IllegalArgumentException if {@code size} is negative + */ + public static int checkElementIndex( + int index, int size, String desc) { + // Carefully optimized for execution by hotspot (explanatory comment above) + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(badElementIndex(index, size, desc)); + } + return index; + } + + private static String badElementIndex(int index, int size, String desc) { + if (index < 0) { + return format("%s (%s) must not be negative", desc, index); + } else if (size < 0) { + throw new IllegalArgumentException("negative size: " + size); + } else { // index >= size + return format("%s (%s) must be less than size (%s)", desc, index, size); + } + } + + /** + * Ensures that {@code index} specifies a valid position in an array, + * list or string of size {@code size}. A position index may range from zero + * to {@code size}, inclusive. + * + * @param index a user-supplied index identifying a position in an array, list + * or string + * @param size the size of that array, list or string + * @return the value of {@code index} + * @throws IndexOutOfBoundsException if {@code index} is negative or is + * greater than {@code size} + * @throws IllegalArgumentException if {@code size} is negative + */ + public static int checkPositionIndex(int index, int size) { + return checkPositionIndex(index, size, "index"); + } + + /** + * Ensures that {@code index} specifies a valid position in an array, + * list or string of size {@code size}. A position index may range from zero + * to {@code size}, inclusive. + * + * @param index a user-supplied index identifying a position in an array, list + * or string + * @param size the size of that array, list or string + * @param desc the text to use to describe this index in an error message + * @return the value of {@code index} + * @throws IndexOutOfBoundsException if {@code index} is negative or is + * greater than {@code size} + * @throws IllegalArgumentException if {@code size} is negative + */ + public static int checkPositionIndex( + int index, int size, String desc) { + // Carefully optimized for execution by hotspot (explanatory comment above) + if (index < 0 || index > size) { + throw new IndexOutOfBoundsException(badPositionIndex(index, size, desc)); + } + return index; + } + + private static String badPositionIndex(int index, int size, String desc) { + if (index < 0) { + return format("%s (%s) must not be negative", desc, index); + } else if (size < 0) { + throw new IllegalArgumentException("negative size: " + size); + } else { // index > size + return format("%s (%s) must not be greater than size (%s)", + desc, index, size); + } + } + + /** + * Ensures that {@code start} and {@code end} specify a valid positions + * in an array, list or string of size {@code size}, and are in order. A + * position index may range from zero to {@code size}, inclusive. + * + * @param start a user-supplied index identifying a starting position in an + * array, list or string + * @param end a user-supplied index identifying a ending position in an array, + * list or string + * @param size the size of that array, list or string + * @throws IndexOutOfBoundsException if either index is negative or is + * greater than {@code size}, or if {@code end} is less than {@code start} + * @throws IllegalArgumentException if {@code size} is negative + */ + public static void checkPositionIndexes(int start, int end, int size) { + // Carefully optimized for execution by hotspot (explanatory comment above) + if (start < 0 || end < start || end > size) { + throw new IndexOutOfBoundsException(badPositionIndexes(start, end, size)); + } + } + + private static String badPositionIndexes(int start, int end, int size) { + if (start < 0 || start > size) { + return badPositionIndex(start, size, "start index"); + } + if (end < 0 || end > size) { + return badPositionIndex(end, size, "end index"); + } + // end < start + return format("end index (%s) must not be less than start index (%s)", + end, start); + } + + /** + * Substitutes each {@code %s} in {@code template} with an argument. These + * are matched by position - the first {@code %s} gets {@code args[0]}, etc. + * If there are more arguments than placeholders, the unmatched arguments will + * be appended to the end of the formatted message in square braces. + * + * @param template a non-null string containing 0 or more {@code %s} + * placeholders. + * @param args the arguments to be substituted into the message + * template. Arguments are converted to strings using + * {@link String#valueOf(Object)}. Arguments can be null. + */ + static String format(String template, + Object... args) { + template = String.valueOf(template); // null -> "null" + + // start substituting the arguments into the '%s' placeholders + StringBuilder builder = new StringBuilder( + template.length() + 16 * args.length); + int templateStart = 0; + int i = 0; + while (i < args.length) { + int placeholderStart = template.indexOf("%s", templateStart); + if (placeholderStart == -1) { + break; + } + builder.append(template.substring(templateStart, placeholderStart)); + builder.append(args[i++]); + templateStart = placeholderStart + 2; + } + builder.append(template.substring(templateStart)); + + // if we run out of placeholders, append the extra args in square braces + if (i < args.length) { + builder.append(" ["); + builder.append(args[i++]); + while (i < args.length) { + builder.append(", "); + builder.append(args[i++]); + } + builder.append(']'); + } + + return builder.toString(); + } +} diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Present.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Present.java new file mode 100644 index 0000000000..630570ff4f --- /dev/null +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Present.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2011 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.whispersystems.libaxolotl.util.guava; + +import static org.whispersystems.libaxolotl.util.guava.Preconditions.checkNotNull; + +import java.util.Collections; +import java.util.Set; + +/** + * Implementation of an {@link Optional} containing a reference. + */ + +final class Present extends Optional { + private final T reference; + + Present(T reference) { + this.reference = reference; + } + + @Override public boolean isPresent() { + return true; + } + + @Override public T get() { + return reference; + } + + @Override public T or(T defaultValue) { + checkNotNull(defaultValue, "use orNull() instead of or(null)"); + return reference; + } + + @Override public Optional or(Optional secondChoice) { + checkNotNull(secondChoice); + return this; + } + + @Override public T or(Supplier supplier) { + checkNotNull(supplier); + return reference; + } + + @Override public T orNull() { + return reference; + } + + @Override public Set asSet() { + return Collections.singleton(reference); + } + + @Override public Optional transform(Function function) { + return new Present(checkNotNull(function.apply(reference), + "Transformation function cannot return null.")); + } + + @Override public boolean equals(Object object) { + if (object instanceof Present) { + Present other = (Present) object; + return reference.equals(other.reference); + } + return false; + } + + @Override public int hashCode() { + return 0x598df91c + reference.hashCode(); + } + + @Override public String toString() { + return "Optional.of(" + reference + ")"; + } + + private static final long serialVersionUID = 0; +} diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Supplier.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Supplier.java new file mode 100644 index 0000000000..ba88070732 --- /dev/null +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/util/guava/Supplier.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.whispersystems.libaxolotl.util.guava; + + +/** + * A class that can supply objects of a single type. Semantically, this could + * be a factory, generator, builder, closure, or something else entirely. No + * guarantees are implied by this interface. + * + * @author Harry Heymann + * @since 2.0 (imported from Google Collections Library) + */ +public interface Supplier { + /** + * Retrieves an instance of the appropriate type. The returned object may or + * may not be a new instance, depending on the implementation. + * + * @return an instance of the appropriate type + */ + T get(); +}