聊天-群聊(2)

4.66.1版本的源码。

UnidentifiedAccess是如何生成的

加密的时候需要用到UnidentifiedAccess,看下UnidentifiedAccess是如何生成的。

//SignalServiceMessageSender.java

  /**
   * Send a message to a single recipient.
   *
   * @param recipient The message's destination.
   * @param message The message.
   * @throws UntrustedIdentityException
   * @throws IOException
   */
  public SendMessageResult sendMessage(SignalServiceAddress             recipient,
                                       Optional<UnidentifiedAccessPair> unidentifiedAccess,
                                       SignalServiceDataMessage         message)
      throws UntrustedIdentityException, IOException
  {
    byte[]            content   = createMessageContent(message);
    long              timestamp = message.getTimestamp();
    SendMessageResult result    = sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, null);

    if (result.getSuccess() != null && result.getSuccess().isNeedsSync()) {
      byte[] syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false);
      sendMessage(localAddress, Optional.<UnidentifiedAccess>absent(), timestamp, syncMessage, false, null);
    }

    if (message.isEndSession()) {
      if (recipient.getUuid().isPresent()) {
        store.deleteAllSessions(recipient.getUuid().get().toString());
      }
      if (recipient.getNumber().isPresent()) {
        store.deleteAllSessions(recipient.getNumber().get());
      }

      if (eventListener.isPresent()) {
        eventListener.get().onSecurityEvent(recipient);
      }
    }

    return result;
  }

看下这个方法是哪里调用的:

//PushMediaSendJob.java

  private boolean deliver(OutgoingMediaMessage message)
      throws RetryLaterException, InsecureFallbackApprovalException, UntrustedIdentityException,
             UndeliverableMessageException
  {
    if (message.getRecipient() == null) {
      throw new UndeliverableMessageException("No destination address.");
    }

    try {
      rotateSenderCertificateIfNecessary();

      Recipient                                  messageRecipient   = message.getRecipient().fresh();
      SignalServiceMessageSender                 messageSender      = ApplicationDependencies.getSignalServiceMessageSender();
      SignalServiceAddress                       address            = getPushAddress(messageRecipient);
      List<Attachment>                           attachments        = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
      List<SignalServiceAttachment>              serviceAttachments = getAttachmentPointersFor(attachments);
      Optional<byte[]>                           profileKey         = getProfileKey(messageRecipient);
      Optional<SignalServiceDataMessage.Quote>   quote              = getQuoteFor(message);
      Optional<SignalServiceDataMessage.Sticker> sticker            = getStickerFor(message);
      List<SharedContact>                        sharedContacts     = getSharedContactsFor(message);
      List<Preview>                              previews           = getPreviewsFor(message);
      SignalServiceDataMessage                   mediaMessage       = SignalServiceDataMessage.newBuilder()
                                                                                            .withBody(message.getBody())
                                                                                            .withAttachments(serviceAttachments)
                                                                                            .withTimestamp(message.getSentTimeMillis())
                                                                                            .withExpiration((int)(message.getExpiresIn() / 1000))
                                                                                            .withViewOnce(message.isViewOnce())
                                                                                            .withProfileKey(profileKey.orNull())
                                                                                            .withQuote(quote.orNull())
                                                                                            .withSticker(sticker.orNull())
                                                                                            .withSharedContacts(sharedContacts)
                                                                                            .withPreviews(previews)
                                                                                            .asExpirationUpdate(message.isExpirationUpdate())
                                                                                            .build();

      if (Util.equals(TextSecurePreferences.getLocalUuid(context), address.getUuid().orNull())) {
        Optional<UnidentifiedAccessPair> syncAccess  = UnidentifiedAccessUtil.getAccessForSync(context);
        SignalServiceSyncMessage         syncMessage = buildSelfSendSyncMessage(context, mediaMessage, syncAccess);

        messageSender.sendMessage(syncMessage, syncAccess);
        return syncAccess.isPresent();
      } else {
      	//发送消息
        return messageSender.sendMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), mediaMessage).getSuccess().isUnidentified();
      }
    } catch (UnregisteredUserException e) {
      warn(TAG, e);
      throw new InsecureFallbackApprovalException(e);
    } catch (FileNotFoundException e) {
      warn(TAG, e);
      throw new UndeliverableMessageException(e);
    } catch (IOException e) {
      warn(TAG, e);
      throw new RetryLaterException(e);
    }
  }

可以看到UnidentifiedAccessUtil.getAccessFor(context, messageRecipient) 根据recipient构建生成了UnidentifiedAccess的pair对。

//UnidentifiedAccessUtil.java

  @WorkerThread
  public static Optional<UnidentifiedAccessPair> getAccessFor(@NonNull Context context,
                                                              @NonNull Recipient recipient)
  {
    try {
      byte[] theirUnidentifiedAccessKey       = getTargetUnidentifiedAccessKey(recipient);
      byte[] ourUnidentifiedAccessKey         = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
      byte[] ourUnidentifiedAccessCertificate = TextSecurePreferences.getUnidentifiedAccessCertificate(context);

      if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
        ourUnidentifiedAccessKey = Util.getSecretBytes(16);
      }

      Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) +
                 " | Our access key present? " + (ourUnidentifiedAccessKey != null) +
                 " | Our certificate present? " + (ourUnidentifiedAccessCertificate != null) +
                 " | UUID certificate supported? " + recipient.isUuidSupported());

      if (theirUnidentifiedAccessKey != null &&
          ourUnidentifiedAccessKey != null   &&
          ourUnidentifiedAccessCertificate != null)
      {
        return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey,
                                                                             ourUnidentifiedAccessCertificate),
                                                      new UnidentifiedAccess(ourUnidentifiedAccessKey,
                                                                             ourUnidentifiedAccessCertificate)));
      }

      return Optional.absent();
    } catch (InvalidCertificateException e) {
      Log.w(TAG, e);
      return Optional.absent();
    }
  }


  private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {
    ProfileKey theirProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.resolve().getProfileKey());

    switch (recipient.resolve().getUnidentifiedAccessMode()) {
      case UNKNOWN:
        if (theirProfileKey == null) return Util.getSecretBytes(16);
        else                         return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
      case DISABLED:
        return null;
      case ENABLED:
        if (theirProfileKey == null) return null;
        else                         return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
      case UNRESTRICTED:
        return Util.getSecretBytes(16);
      default:
        throw new AssertionError("Unknown mode: " + recipient.getUnidentifiedAccessMode().getMode());
    }
  }

加密过程

//SignalServiceMessageSender.java


  private OutgoingPushMessage getEncryptedMessage(PushServiceSocket            socket,
                                                  SignalServiceAddress         recipient,
                                                  Optional<UnidentifiedAccess> unidentifiedAccess,
                                                  int                          deviceId,
                                                  byte[]                       plaintext)
      throws IOException, InvalidKeyException, UntrustedIdentityException
  {
    SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getIdentifier(), deviceId);
    SignalServiceCipher   cipher                = new SignalServiceCipher(localAddress, store, null);

    if (!store.containsSession(signalProtocolAddress)) {
      try {
        List<PreKeyBundle> preKeys = socket.getPreKeys(recipient, unidentifiedAccess, deviceId);

        for (PreKeyBundle preKey : preKeys) {
          try {
            SignalProtocolAddress preKeyAddress  = new SignalProtocolAddress(recipient.getIdentifier(), preKey.getDeviceId());
            SessionBuilder        sessionBuilder = new SessionBuilder(store, preKeyAddress);
            sessionBuilder.process(preKey);
          } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
            throw new UntrustedIdentityException("Untrusted identity key!", recipient.getIdentifier(), preKey.getIdentityKey());
          }
        }

        if (eventListener.isPresent()) {
          eventListener.get().onSecurityEvent(recipient);
        }
      } catch (InvalidKeyException e) {
        throw new IOException(e);
      }
    }

    try {
      return cipher.encrypt(signalProtocolAddress, unidentifiedAccess, plaintext);
    } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
      throw new UntrustedIdentityException("Untrusted on send", recipient.getIdentifier(), e.getUntrustedIdentity());
    }
  }

看下cipher.encrypt()加密过程:

//SignalServiceCipher.java

  public OutgoingPushMessage encrypt(SignalProtocolAddress        destination,
                                     Optional<UnidentifiedAccess> unidentifiedAccess,
                                     byte[]                       unpaddedMessage)
      throws UntrustedIdentityException, InvalidKeyException
  {
  	//1.如果有unidentifiedAccess
    if (unidentifiedAccess.isPresent()) {
      SealedSessionCipher  sessionCipher        = new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1);
      PushTransportDetails transportDetails     = new PushTransportDetails(sessionCipher.getSessionVersion(destination));
      byte[]               ciphertext           = sessionCipher.encrypt(destination, unidentifiedAccess.get().getUnidentifiedCertificate(), transportDetails.getPaddedMessageBody(unpaddedMessage));
      String               body                 = Base64.encodeBytes(ciphertext);
      int                  remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination);

      return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
    } else { //2.如果没有unidentifiedAccess
      SessionCipher        sessionCipher        = new SessionCipher(signalProtocolStore, destination);
      PushTransportDetails transportDetails     = new PushTransportDetails(sessionCipher.getSessionVersion());
      CiphertextMessage    message              = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
      int                  remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
      String               body                 = Base64.encodeBytes(message.serialize());

      int type;

      switch (message.getType()) {
        case CiphertextMessage.PREKEY_TYPE:  type = Type.PREKEY_BUNDLE_VALUE; break;
        case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE;    break;
        default: throw new AssertionError("Bad type: " + message.getType());
      }

      return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body);
    }
  }

先看1.如果有unidentifiedAccess 的这条路径:

调用sessionCipher.encrypt进行加密,sessionCipher是SealedSessionCipher:

//SealedSessionCipher.java

  public byte[] encrypt(SignalProtocolAddress destinationAddress, SenderCertificate senderCertificate, byte[] paddedPlaintext)
      throws InvalidKeyException, UntrustedIdentityException
  {
  	//1.先调用SessionCipher的encrypt()方法对消息数据进行加密
    CiphertextMessage message       = new SessionCipher(signalProtocolStore, destinationAddress).encrypt(paddedPlaintext);
    //消息发送方的公私钥对
    IdentityKeyPair   ourIdentity   = signalProtocolStore.getIdentityKeyPair();
    //消息接收方的公钥
    ECPublicKey       theirIdentity = signalProtocolStore.getIdentity(destinationAddress).getPublicKey();
	
	
	//2.生成一个临时的新的Curve25519椭圆曲线的公钥/私钥对
    ECKeyPair     ephemeral           = Curve.generateKeyPair();
    byte[]        ephemeralSalt       = ByteUtil.combine("UnidentifiedDelivery".getBytes(), theirIdentity.serialize(), ephemeral.getPublicKey().serialize());
    //计算临时的EphemeralKeys,会用到ECDH协商
    EphemeralKeys ephemeralKeys       = calculateEphemeralKeys(theirIdentity, ephemeral.getPrivateKey(), ephemeralSalt);
    //对发送方的公钥进行加密
    byte[]        staticKeyCiphertext = encrypt(ephemeralKeys.cipherKey, ephemeralKeys.macKey, ourIdentity.getPublicKey().getPublicKey().serialize());

    //3.加密
    byte[]                           staticSalt   = ByteUtil.combine(ephemeralKeys.chainKey, staticKeyCiphertext);
    StaticKeys                       staticKeys   = calculateStaticKeys(theirIdentity, ourIdentity.getPrivateKey(), staticSalt);
    UnidentifiedSenderMessageContent content      = new UnidentifiedSenderMessageContent(message.getType(), senderCertificate, message.serialize());
    //3.对消息数据进行加密
    byte[]                           messageBytes = encrypt(staticKeys.cipherKey, staticKeys.macKey, content.getSerialized());

    return new UnidentifiedSenderMessage(ephemeral.getPublicKey(), staticKeyCiphertext, messageBytes).getSerialized();
  }
  1. 先调用SessionCipher的encrypt()方法进行加密,下面的路径2会具体分析到这个方法。

  2. 生成一个临时的新的Curve25519椭圆曲线的公钥/私钥对

//Curve25519.java

  /**
   * Calculates an ECDH agreement.
   *
   * @param publicKey The Curve25519 (typically remote party's) public key.
   * @param privateKey The Curve25519 (typically yours) private key.
   * @return A 32-byte shared secret.
   */
  public byte[] calculateAgreement(byte[] publicKey, byte[] privateKey) {
    if (publicKey == null || privateKey == null) {
      throw new IllegalArgumentException("Keys must not be null!");
    }

    if (publicKey.length != 32 || privateKey.length != 32) {
      throw new IllegalArgumentException("Keys must be 32 bytes!");
    }

    return provider.calculateAgreement(privateKey, publicKey);
  }
  1. 对消息数据进行加密

再看下路径 2.如果没有unidentifiedAccess 的这条路径:

调用sessionCipher.encrypt进行加密,sessionCipher是SessionCipher:

//SessionCipher.java


  /**
   * Encrypt a message.
   *
   * @param  paddedMessage The plaintext message bytes, optionally padded to a constant multiple.
   * @return A ciphertext message encrypted to the recipient+device tuple.
   */
  public CiphertextMessage encrypt(byte[] paddedMessage) throws UntrustedIdentityException {
    synchronized (SESSION_LOCK) {
      SessionRecord sessionRecord   = sessionStore.loadSession(remoteAddress);
      SessionState  sessionState    = sessionRecord.getSessionState();
      ChainKey      chainKey        = sessionState.getSenderChainKey();
      MessageKeys   messageKeys     = chainKey.getMessageKeys();
      ECPublicKey   senderEphemeral = sessionState.getSenderRatchetKey();
      int           previousCounter = sessionState.getPreviousCounter();
      int           sessionVersion  = sessionState.getSessionVersion();
		
	  //进行加密	
      byte[]            ciphertextBody    = getCiphertext(messageKeys, paddedMessage);
      CiphertextMessage ciphertextMessage = new SignalMessage(sessionVersion, messageKeys.getMacKey(),
                                                              senderEphemeral, chainKey.getIndex(),
                                                              previousCounter, ciphertextBody,
                                                              sessionState.getLocalIdentityKey(),
                                                              sessionState.getRemoteIdentityKey());

      if (sessionState.hasUnacknowledgedPreKeyMessage()) {
        UnacknowledgedPreKeyMessageItems items = sessionState.getUnacknowledgedPreKeyMessageItems();
        int localRegistrationId = sessionState.getLocalRegistrationId();

        ciphertextMessage = new PreKeySignalMessage(sessionVersion, localRegistrationId, items.getPreKeyId(),
                                                    items.getSignedPreKeyId(), items.getBaseKey(),
                                                    sessionState.getLocalIdentityKey(),
                                                    (SignalMessage) ciphertextMessage);
      }

      sessionState.setSenderChainKey(chainKey.getNextChainKey());

      if (!identityKeyStore.isTrustedIdentity(remoteAddress, sessionState.getRemoteIdentityKey(), IdentityKeyStore.Direction.SENDING)) {
        throw new UntrustedIdentityException(remoteAddress.getName(), sessionState.getRemoteIdentityKey());
      }

      identityKeyStore.saveIdentity(remoteAddress, sessionState.getRemoteIdentityKey());
      sessionStore.storeSession(remoteAddress, sessionRecord);
      return ciphertextMessage;
    }
  }

看下getCiphertext()方法:

  private byte[] getCiphertext(MessageKeys messageKeys, byte[] plaintext) {
    try {
      Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getIv());
      return cipher.doFinal(plaintext);
    } catch (IllegalBlockSizeException | BadPaddingException e) {
      throw new AssertionError(e);
    }
  }

调用getCipher()获取Cipher对象:

  private Cipher getCipher(int mode, SecretKeySpec key, IvParameterSpec iv) {
    try {
      Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
      cipher.init(mode, key, iv);
      return cipher;
    } catch (NoSuchAlgorithmException | NoSuchPaddingException | java.security.InvalidKeyException |
             InvalidAlgorithmParameterException e)
    {
      throw new AssertionError(e);
    }
  }

获取的是 “AES/CBC/PKCS5Padding”

然后执行cipher.doFinal()进行加密,将结果返回。

解密过程

//SealedSessionCipher.java

  public DecryptionResult decrypt(CertificateValidator validator, byte[] ciphertext, long timestamp)
      throws
      InvalidMetadataMessageException, InvalidMetadataVersionException,
      ProtocolInvalidMessageException, ProtocolInvalidKeyException,
      ProtocolNoSessionException, ProtocolLegacyMessageException,
      ProtocolInvalidVersionException, ProtocolDuplicateMessageException,
      ProtocolInvalidKeyIdException, ProtocolUntrustedIdentityException,
      SelfSendException
  {
    UnidentifiedSenderMessageContent content;

    try {
      // 公/私钥对
      IdentityKeyPair           ourIdentity    = signalProtocolStore.getIdentityKeyPair();
      UnidentifiedSenderMessage wrapper        = new UnidentifiedSenderMessage(ciphertext);
      byte[]                    ephemeralSalt  = ByteUtil.combine("UnidentifiedDelivery".getBytes(), ourIdentity.getPublicKey().getPublicKey().serialize(), wrapper.getEphemeral().serialize());
      EphemeralKeys             ephemeralKeys  = calculateEphemeralKeys(wrapper.getEphemeral(), ourIdentity.getPrivateKey(), ephemeralSalt);
      //解密staticKey
      byte[]                    staticKeyBytes = decrypt(ephemeralKeys.cipherKey, ephemeralKeys.macKey, wrapper.getEncryptedStatic());

      ECPublicKey staticKey    = Curve.decodePoint(staticKeyBytes, 0);
      byte[]      staticSalt   = ByteUtil.combine(ephemeralKeys.chainKey, wrapper.getEncryptedStatic());
      StaticKeys  staticKeys   = calculateStaticKeys(staticKey, ourIdentity.getPrivateKey(), staticSalt);
      //解密消息数据
      byte[]      messageBytes = decrypt(staticKeys.cipherKey, staticKeys.macKey, wrapper.getEncryptedMessage());

	  //验签,对服务器验签 & 对发送者验签
      content = new UnidentifiedSenderMessageContent(messageBytes);
      validator.validate(content.getSenderCertificate(), timestamp);

      if (!MessageDigest.isEqual(content.getSenderCertificate().getKey().serialize(), staticKeyBytes)) {
        throw new InvalidKeyException("Sender's certificate key does not match key used in message");
      }

      boolean isLocalE164 = localE164Address != null && localE164Address.equals(content.getSenderCertificate().getSenderE164().orNull());
      boolean isLocalUuid = localUuidAddress != null && localUuidAddress.equals(content.getSenderCertificate().getSenderUuid().orNull());

      if ((isLocalE164 || isLocalUuid) && content.getSenderCertificate().getSenderDeviceId() == localDeviceId) {
        throw new SelfSendException();
      }
    } catch (InvalidKeyException | InvalidMacException | InvalidCertificateException e) {
      throw new InvalidMetadataMessageException(e);
    }

    try {
      //解密并返回解密后的消息数据,四要素:发送者的uuid,发送者的e164,发送者的设备id,发送的消息内容(解密后的)
      return new DecryptionResult(content.getSenderCertificate().getSenderUuid(),
                                  content.getSenderCertificate().getSenderE164(),
                                  content.getSenderCertificate().getSenderDeviceId(),
                                  decrypt(content));
    } catch (InvalidMessageException e) {
      throw new ProtocolInvalidMessageException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (InvalidKeyException e) {
      throw new ProtocolInvalidKeyException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (NoSessionException e) {
      throw new ProtocolNoSessionException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (LegacyMessageException e) {
      throw new ProtocolLegacyMessageException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (InvalidVersionException e) {
      throw new ProtocolInvalidVersionException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (DuplicateMessageException e) {
      throw new ProtocolDuplicateMessageException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (InvalidKeyIdException e) {
      throw new ProtocolInvalidKeyIdException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (UntrustedIdentityException e) {
      throw new ProtocolUntrustedIdentityException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    }
  }

SignalProtocolAddress

public class SignalProtocolAddress {

  private final String name;
  private final int    deviceId;

  public SignalProtocolAddress(String name, int deviceId) {
    this.name     = name;
    this.deviceId = deviceId;
  }

  public String getName() {
    return name;
  }

  public int getDeviceId() {
    return deviceId;
  }

  @Override
  public String toString() {
    return name + ":" + deviceId;
  }

  @Override
  public boolean equals(Object other) {
    if (other == null)                       return false;
    if (!(other instanceof SignalProtocolAddress)) return false;

    SignalProtocolAddress that = (SignalProtocolAddress)other;
    return this.name.equals(that.name) && this.deviceId == that.deviceId;
  }

  @Override
  public int hashCode() {
    return this.name.hashCode() ^ this.deviceId;
  }
}

协议层的发送地址是SignalProtocolAddress,其中name要么是uuid,要么e164(国际手机号),deviceId就是设备id,用于区分不同的设备。

接收方的公钥存储

在上面分析的发送消息时的消息加密过程中知道,加密需要用到消息接收方的公钥,而消息接收方的公钥是通过signalProtocolStore.getIdentity(destinationAddress).getPublicKey() 获取到的:

//SealedSessionCipher.java

  public byte[] encrypt(SignalProtocolAddress destinationAddress, SenderCertificate senderCertificate, byte[] paddedPlaintext)
      throws InvalidKeyException, UntrustedIdentityException
  {
  	//先调用SessionCipher的encrypt()方法进行加密
    CiphertextMessage message       = new SessionCipher(signalProtocolStore, destinationAddress).encrypt(paddedPlaintext);
    IdentityKeyPair   ourIdentity   = signalProtocolStore.getIdentityKeyPair();
    //消息接收方的公钥
    ECPublicKey       theirIdentity = signalProtocolStore.getIdentity(destinationAddress).getPublicKey();
	//生成一个新的Curve25519椭圆曲线的公钥/私钥对
    ECKeyPair     ephemeral           = Curve.generateKeyPair();
    byte[]        ephemeralSalt       = ByteUtil.combine("UnidentifiedDelivery".getBytes(), theirIdentity.serialize(), ephemeral.getPublicKey().serialize());
    EphemeralKeys ephemeralKeys       = calculateEphemeralKeys(theirIdentity, ephemeral.getPrivateKey(), ephemeralSalt);
    byte[]        staticKeyCiphertext = encrypt(ephemeralKeys.cipherKey, ephemeralKeys.macKey, ourIdentity.getPublicKey().getPublicKey().serialize());

    byte[]                           staticSalt   = ByteUtil.combine(ephemeralKeys.chainKey, staticKeyCiphertext);
    StaticKeys                       staticKeys   = calculateStaticKeys(theirIdentity, ourIdentity.getPrivateKey(), staticSalt);
    UnidentifiedSenderMessageContent content      = new UnidentifiedSenderMessageContent(message.getType(), senderCertificate, message.serialize());
    byte[]                           messageBytes = encrypt(staticKeys.cipherKey, staticKeys.macKey, content.getSerialized());

    return new UnidentifiedSenderMessage(ephemeral.getPublicKey(), staticKeyCiphertext, messageBytes).getSerialized();
  }
  

signalProtocolStore.getIdentity()获取IdentityKey,IdentityKey就是对PublicKey的封装。

那么signalProtocolStore中的公钥数据是什么时候存进去的呢?

//SignalProtocolStoreImpl.java

public class SignalProtocolStoreImpl implements SignalProtocolStore {

 
  @Override
  public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
    return identityKeyStore.saveIdentity(address, identityKey);
  }

一条路径是当接收到 PREKEY_TYPE 类型的消息时:

//SealedSessionCipher.java


  private byte[] decrypt(UnidentifiedSenderMessageContent message)
      throws InvalidVersionException, InvalidMessageException, InvalidKeyException, DuplicateMessageException, InvalidKeyIdException, UntrustedIdentityException, LegacyMessageException, NoSessionException
  {
    SignalProtocolAddress sender = getPreferredAddress(signalProtocolStore, message.getSenderCertificate());

    switch (message.getType()) {
      case CiphertextMessage.WHISPER_TYPE: return new SessionCipher(signalProtocolStore, sender).decrypt(new SignalMessage(message.getContent()));
      case CiphertextMessage.PREKEY_TYPE:  return new SessionCipher(signalProtocolStore, sender).decrypt(new PreKeySignalMessage(message.getContent()));
      default:                             throw new InvalidMessageException("Unknown type: " + message.getType());
    }
  }

消息的内容message.getContent() 就包含有发送者的publickey,

//SessionCipher.java

  public byte[] decrypt(SignalMessage ciphertext, DecryptionCallback callback)
      throws InvalidMessageException, DuplicateMessageException, LegacyMessageException,
             NoSessionException, UntrustedIdentityException
  {
    synchronized (SESSION_LOCK) {

      if (!sessionStore.containsSession(remoteAddress)) {
        throw new NoSessionException("No session for: " + remoteAddress);
      }

      SessionRecord sessionRecord = sessionStore.loadSession(remoteAddress);
      byte[]        plaintext     = decrypt(sessionRecord, ciphertext);

      if (!identityKeyStore.isTrustedIdentity(remoteAddress, sessionRecord.getSessionState().getRemoteIdentityKey(), IdentityKeyStore.Direction.RECEIVING)) {
        throw new UntrustedIdentityException(remoteAddress.getName(), sessionRecord.getSessionState().getRemoteIdentityKey());
      }

      identityKeyStore.saveIdentity(remoteAddress, sessionRecord.getSessionState().getRemoteIdentityKey());

      callback.handlePlaintext(plaintext);

      sessionStore.storeSession(remoteAddress, sessionRecord);

      return plaintext;
    }
  }

上述路径中,当接收到 WHISPER_TYPE 类型的消息时,也会存储:

//SessionCipher.java

  /**
   * Decrypt a message.
   *
   * @param  ciphertext The {@link SignalMessage} to decrypt.
   *
   * @return The plaintext.
   * @throws InvalidMessageException if the input is not valid ciphertext.
   * @throws DuplicateMessageException if the input is a message that has already been received.
   * @throws LegacyMessageException if the input is a message formatted by a protocol version that
   *                                is no longer supported.
   * @throws NoSessionException if there is no established session for this contact.
   */
  public byte[] decrypt(SignalMessage ciphertext)
      throws InvalidMessageException, DuplicateMessageException, LegacyMessageException,
      NoSessionException, UntrustedIdentityException
  {
    return decrypt(ciphertext, new NullDecryptionCallback());
  }

  public byte[] decrypt(SignalMessage ciphertext, DecryptionCallback callback)
      throws InvalidMessageException, DuplicateMessageException, LegacyMessageException,
             NoSessionException, UntrustedIdentityException
  {
    synchronized (SESSION_LOCK) {

      if (!sessionStore.containsSession(remoteAddress)) {
        throw new NoSessionException("No session for: " + remoteAddress);
      }

      SessionRecord sessionRecord = sessionStore.loadSession(remoteAddress);
      byte[]        plaintext     = decrypt(sessionRecord, ciphertext);

      if (!identityKeyStore.isTrustedIdentity(remoteAddress, sessionRecord.getSessionState().getRemoteIdentityKey(), IdentityKeyStore.Direction.RECEIVING)) {
        throw new UntrustedIdentityException(remoteAddress.getName(), sessionRecord.getSessionState().getRemoteIdentityKey());
      }
			
		//存储IdentityKey
      identityKeyStore.saveIdentity(remoteAddress, sessionRecord.getSessionState().getRemoteIdentityKey());

      callback.handlePlaintext(plaintext);

      sessionStore.storeSession(remoteAddress, sessionRecord);

      return plaintext;
    }
  }

SenderKeyMessage

SenderKeyMessage 的Type类型 就是 SENDERKEY_TYPE :

public class SenderKeyMessage implements CiphertextMessage {

  private static final int SIGNATURE_LENGTH = 64;

  private final int         messageVersion;
  private final int         keyId;
  private final int         iteration;
  private final byte[]      ciphertext;
  private final byte[]      serialized;

  public SenderKeyMessage(byte[] serialized) throws InvalidMessageException, LegacyMessageException {
    try {
      byte[][] messageParts = ByteUtil.split(serialized, 1, serialized.length - 1 - SIGNATURE_LENGTH, SIGNATURE_LENGTH);
      byte     version      = messageParts[0][0];
      byte[]   message      = messageParts[1];
      byte[]   signature    = messageParts[2];

      if (ByteUtil.highBitsToInt(version) < 3) {
        throw new LegacyMessageException("Legacy message: " + ByteUtil.highBitsToInt(version));
      }

      if (ByteUtil.highBitsToInt(version) > CURRENT_VERSION) {
        throw new InvalidMessageException("Unknown version: " + ByteUtil.highBitsToInt(version));
      }

      SignalProtos.SenderKeyMessage senderKeyMessage = SignalProtos.SenderKeyMessage.parseFrom(message);

      if (!senderKeyMessage.hasId() ||
          !senderKeyMessage.hasIteration() ||
          !senderKeyMessage.hasCiphertext())
      {
        throw new InvalidMessageException("Incomplete message.");
      }

      this.serialized     = serialized;
      this.messageVersion = ByteUtil.highBitsToInt(version);
      this.keyId          = senderKeyMessage.getId();
      this.iteration      = senderKeyMessage.getIteration();
      this.ciphertext     = senderKeyMessage.getCiphertext().toByteArray();
    } catch (InvalidProtocolBufferException | ParseException e) {
      throw new InvalidMessageException(e);
    }
  }

  public SenderKeyMessage(int keyId, int iteration, byte[] ciphertext, ECPrivateKey signatureKey) {
    byte[] version = {ByteUtil.intsToByteHighAndLow(CURRENT_VERSION, CURRENT_VERSION)};
    byte[] message = SignalProtos.SenderKeyMessage.newBuilder()
                                                  .setId(keyId)
                                                  .setIteration(iteration)
                                                  .setCiphertext(ByteString.copyFrom(ciphertext))
                                                  .build().toByteArray();

    byte[] signature = getSignature(signatureKey, ByteUtil.combine(version, message));

    this.serialized       = ByteUtil.combine(version, message, signature);
    this.messageVersion   = CURRENT_VERSION;
    this.keyId            = keyId;
    this.iteration        = iteration;
    this.ciphertext       = ciphertext;
  }

  public int getKeyId() {
    return keyId;
  }

  public int getIteration() {
    return iteration;
  }

  public byte[] getCipherText() {
    return ciphertext;
  }

  public void verifySignature(ECPublicKey signatureKey)
      throws InvalidMessageException
  {
    try {
      byte[][] parts    = ByteUtil.split(serialized, serialized.length - SIGNATURE_LENGTH, SIGNATURE_LENGTH);

      if (!Curve.verifySignature(signatureKey, parts[0], parts[1])) {
        throw new InvalidMessageException("Invalid signature!");
      }

    } catch (InvalidKeyException e) {
      throw new InvalidMessageException(e);
    }
  }

  private byte[] getSignature(ECPrivateKey signatureKey, byte[] serialized) {
    try {
      return Curve.calculateSignature(signatureKey, serialized);
    } catch (InvalidKeyException e) {
      throw new AssertionError(e);
    }
  }

  @Override
  public byte[] serialize() {
    return serialized;
  }

  @Override
  public int getType() {
    return CiphertextMessage.SENDERKEY_TYPE;
  }
}

SenderKeyMessage这种类型的消息什么时候发送呢?

SenderKeyMessage在GroupCipher使用到:

/**
 * The main entry point for Signal Protocol group encrypt/decrypt operations.
 *
 * Once a session has been established with {@link org.whispersystems.libsignal.groups.GroupSessionBuilder}
 * and a {@link org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage} has been
 * distributed to each member of the group, this class can be used for all subsequent encrypt/decrypt
 * operations within that session (ie: until group membership changes).
 *
 * @author Moxie Marlinspike
 */
public class GroupCipher {

  static final Object LOCK = new Object();

  private final SenderKeyStore senderKeyStore;
  private final SenderKeyName senderKeyId;

  public GroupCipher(SenderKeyStore senderKeyStore, SenderKeyName senderKeyId) {
    this.senderKeyStore = senderKeyStore;
    this.senderKeyId    = senderKeyId;
  }

  /**
   * Encrypt a message.
   *
   * @param paddedPlaintext The plaintext message bytes, optionally padded.
   * @return Ciphertext.
   * @throws NoSessionException
   */
  public byte[] encrypt(byte[] paddedPlaintext) throws NoSessionException {
    synchronized (LOCK) {
      try {
        SenderKeyRecord  record         = senderKeyStore.loadSenderKey(senderKeyId);
        SenderKeyState   senderKeyState = record.getSenderKeyState();
        SenderMessageKey senderKey      = senderKeyState.getSenderChainKey().getSenderMessageKey();
        byte[]           ciphertext     = getCipherText(senderKey.getIv(), senderKey.getCipherKey(), paddedPlaintext);

        SenderKeyMessage senderKeyMessage = new SenderKeyMessage(senderKeyState.getKeyId(),
                                                                 senderKey.getIteration(),
                                                                 ciphertext,
                                                                 senderKeyState.getSigningKeyPrivate());

        senderKeyState.setSenderChainKey(senderKeyState.getSenderChainKey().getNext());

        senderKeyStore.storeSenderKey(senderKeyId, record);

        return senderKeyMessage.serialize();
      } catch (InvalidKeyIdException e) {
        throw new NoSessionException(e);
      }
    }
  }

  /**
   * Decrypt a SenderKey group message.
   *
   * @param senderKeyMessageBytes The received ciphertext.
   * @return Plaintext
   * @throws LegacyMessageException
   * @throws InvalidMessageException
   * @throws DuplicateMessageException
   */
  public byte[] decrypt(byte[] senderKeyMessageBytes)
      throws LegacyMessageException, DuplicateMessageException, InvalidMessageException, NoSessionException
  {
    return decrypt(senderKeyMessageBytes, new NullDecryptionCallback());
  }

  /**
   * Decrypt a SenderKey group message.
   *
   * @param senderKeyMessageBytes The received ciphertext.
   * @param callback   A callback that is triggered after decryption is complete,
   *                    but before the updated session state has been committed to the session
   *                    DB.  This allows some implementations to store the committed plaintext
   *                    to a DB first, in case they are concerned with a crash happening between
   *                    the time the session state is updated but before they're able to store
   *                    the plaintext to disk.
   * @return Plaintext
   * @throws LegacyMessageException
   * @throws InvalidMessageException
   * @throws DuplicateMessageException
   */
  public byte[] decrypt(byte[] senderKeyMessageBytes, DecryptionCallback callback)
      throws LegacyMessageException, InvalidMessageException, DuplicateMessageException,
             NoSessionException
  {
    synchronized (LOCK) {
      try {
        SenderKeyRecord record = senderKeyStore.loadSenderKey(senderKeyId);

        if (record.isEmpty()) {
          throw new NoSessionException("No sender key for: " + senderKeyId);
        }

        SenderKeyMessage senderKeyMessage = new SenderKeyMessage(senderKeyMessageBytes);
        SenderKeyState   senderKeyState   = record.getSenderKeyState(senderKeyMessage.getKeyId());

        senderKeyMessage.verifySignature(senderKeyState.getSigningKeyPublic());

        SenderMessageKey senderKey = getSenderKey(senderKeyState, senderKeyMessage.getIteration());

        byte[] plaintext = getPlainText(senderKey.getIv(), senderKey.getCipherKey(), senderKeyMessage.getCipherText());

        callback.handlePlaintext(plaintext);

        senderKeyStore.storeSenderKey(senderKeyId, record);

        return plaintext;
      } catch (org.whispersystems.libsignal.InvalidKeyException | InvalidKeyIdException e) {
        throw new InvalidMessageException(e);
      }
    }
  }

  private SenderMessageKey getSenderKey(SenderKeyState senderKeyState, int iteration)
      throws DuplicateMessageException, InvalidMessageException
  {
    SenderChainKey senderChainKey = senderKeyState.getSenderChainKey();

    if (senderChainKey.getIteration() > iteration) {
      if (senderKeyState.hasSenderMessageKey(iteration)) {
        return senderKeyState.removeSenderMessageKey(iteration);
      } else {
        throw new DuplicateMessageException("Received message with old counter: " +
                                            senderChainKey.getIteration() + " , " + iteration);
      }
    }

    if (iteration - senderChainKey.getIteration() > 2000) {
      throw new InvalidMessageException("Over 2000 messages into the future!");
    }

    while (senderChainKey.getIteration() < iteration) {
      senderKeyState.addSenderMessageKey(senderChainKey.getSenderMessageKey());
      senderChainKey = senderChainKey.getNext();
    }

    senderKeyState.setSenderChainKey(senderChainKey.getNext());
    return senderChainKey.getSenderMessageKey();
  }

  private byte[] getPlainText(byte[] iv, byte[] key, byte[] ciphertext)
      throws InvalidMessageException
  {
    try {
      IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
      Cipher          cipher          = Cipher.getInstance("AES/CBC/PKCS5Padding");

      cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), ivParameterSpec);

      return cipher.doFinal(ciphertext);
    } catch (NoSuchAlgorithmException | NoSuchPaddingException | java.security.InvalidKeyException |
             InvalidAlgorithmParameterException e)
    {
      throw new AssertionError(e);
    } catch (IllegalBlockSizeException | BadPaddingException e) {
      throw new InvalidMessageException(e);
    }
  }

  private byte[] getCipherText(byte[] iv, byte[] key, byte[] plaintext) {
    try {
      IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
      Cipher          cipher          = Cipher.getInstance("AES/CBC/PKCS5Padding");

      cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), ivParameterSpec);

      return cipher.doFinal(plaintext);
    } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException |
             IllegalBlockSizeException | BadPaddingException | java.security.InvalidKeyException e)
    {
      throw new AssertionError(e);
    }
  }

  private static class NullDecryptionCallback implements DecryptionCallback {
    @Override
    public void handlePlaintext(byte[] plaintext) {}
  }

}

根据这个类的注释,当与一个群建立session时,就会给群中的每个成员发送一个 SenderKeyDistributionMessage 消息,然后,后续的群聊消息的加密/解密过程就使用GroupCipher这个类。

看下SenderKeyDistributionMessage的类结构:

public class SenderKeyDistributionMessage implements CiphertextMessage {

  private final int         id;
  private final int         iteration;
  private final byte[]      chainKey;
  private final ECPublicKey signatureKey;
  private final byte[]      serialized;


...

总结就是,当某个人与群新建立session时,服务器就会给群中的每一个成员发送一条消息,该消息包含这个新加入成员的公钥,然后所有客户端接收到消息后都会保存该公钥。

获取PreKey的接口:getPreKeys

/v2/keys/%s/%s

在发送消息时,对消息进行加密时,判断如果SignalServiceProtocolStore store;存储中没有当前session时,会调用这个接口,

//SealedSessionCipher.java

  private OutgoingPushMessage getEncryptedMessage(PushServiceSocket            socket,
                                                  SignalServiceAddress         recipient,
                                                  Optional<UnidentifiedAccess> unidentifiedAccess,
                                                  int                          deviceId,
                                                  byte[]                       plaintext)
      throws IOException, InvalidKeyException, UntrustedIdentityException
  {
    SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getIdentifier(), deviceId);
    SignalServiceCipher   cipher                = new SignalServiceCipher(localAddress, store, sessionLock, null);

    if (!store.containsSession(signalProtocolAddress)) {
      try {
        List<PreKeyBundle> preKeys = socket.getPreKeys(recipient, unidentifiedAccess, deviceId);

        for (PreKeyBundle preKey : preKeys) {
          try {
            SignalProtocolAddress preKeyAddress  = new SignalProtocolAddress(recipient.getIdentifier(), preKey.getDeviceId());
            SignalSessionBuilder  sessionBuilder = new SignalSessionBuilder(sessionLock, new SessionBuilder(store, preKeyAddress));
            sessionBuilder.process(preKey);
          } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
            throw new UntrustedIdentityException("Untrusted identity key!", recipient.getIdentifier(), preKey.getIdentityKey());
          }
        }

        if (eventListener.isPresent()) {
          eventListener.get().onSecurityEvent(recipient);
        }
      } catch (InvalidKeyException e) {
        throw new IOException(e);
      }
    }

	try {
      return cipher.encrypt(signalProtocolAddress, unidentifiedAccess, plaintext);
    } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
      throw new UntrustedIdentityException("Untrusted on send", recipient.getIdentifier(), e.getUntrustedIdentity());
    }

...


即,如果发送消息时发现没有接收方的公钥,必须要先调用getPreKeys接口向服务器获取到对方的PreKey(包含公钥)后才能执行后续的消息发送流程。

PreKey

public class PreKeyEntity {

  @JsonProperty
  private int keyId;

  @JsonProperty
  @JsonSerialize(using = ECPublicKeySerializer.class)
  @JsonDeserialize(using = ECPublicKeyDeserializer.class)
  private ECPublicKey publicKey;

  public PreKeyEntity() {}

  public PreKeyEntity(int keyId, ECPublicKey publicKey) {
    this.keyId     = keyId;
    this.publicKey = publicKey;
  }

  public int getKeyId() {
    return keyId;
  }

  public ECPublicKey getPublicKey() {
    return publicKey;
  }

  private static class ECPublicKeySerializer extends JsonSerializer<ECPublicKey> {
    @Override
    public void serialize(ECPublicKey value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
      gen.writeString(Base64.encodeBytesWithoutPadding(value.serialize()));
    }
  }

  private static class ECPublicKeyDeserializer extends JsonDeserializer<ECPublicKey> {
    @Override
    public ECPublicKey deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
      try {
        return Curve.decodePoint(Base64.decodeWithoutPadding(p.getValueAsString()), 0);
      } catch (InvalidKeyException e) {
        throw new IOException(e);
      }
    }
  }
}

PreKey

协议使用了称为“ PreKeys”的概念。PreKey是存储在服务器中的ECPublicKey和关联的唯一ID(如上图的结构)。PreKeys也可以被签名。

在安装时,客户端会生成单个已签名的PreKey以及大量未签名的PreKey,并将它们全部传输到服务器。

Sessions

协议是面向会话的。客户端建立一个“会话”,然后将其用于所有后续的加密/解密操作。建立会话后,无需拆除会话。

通过以下三种方式的其中一种可以建立会话:

  • PreKeyBundles。希望向收件人发送消息的客户端可以通过从服务器检索该收件人的PreKeyBundle来建立会话。
  • PreKeySignalMessages。客户端可以从收件人接收PreKeySignalMessage消息并使用它来建立会话。
  • KeyExchangeMes​​sages。两个客户端可以交换KeyExchange消息来建立会话。

State

建立的会话封装了两个客户端之间的许多状态。该状态保存在持久记录中,在会话的整个生命周期中都需要保留这些记录。

状态保存在以下位置:

  • 身份状态。客户端将需要维护自己的身份密钥对,以及从其他客户端收到的身份密钥的状态。
  • PreKey的状态。客户将需要维护其生成的PreKey的状态。
  • 已签名的PreKey状态。客户将需要维护其已签名的PreKey的状态。
  • 会话状态。客户将需要保持他们已建立的会话的状态。

更多相关内容可参看:
https://github.com/signalapp/libsignal-protocol-java
whatsapp协议简单分析之-端对端加密

自己的公钥什么时候生成以及发给服务器?

单聊时,给对方发送消息时是要用对方的公钥加密的,然后对方收到消息后才能用自己的私钥解开,那么自己的公钥是什么时候发送给服务器的?(自己的公钥发送给服务器的逻辑其实就是接收方的公钥发送给服务器的逻辑)

账户登录时的验证账户的操作会向服务端注册公钥。

自己的私钥什么时候生成以及保存在哪?

以下是v5.11.3的源码

//IdentityKeyUtil.java

/**
 * Utility class for working with identity keys.
 * 
 * @author Moxie Marlinspike
 */

public class IdentityKeyUtil {


  ...


  public static @NonNull IdentityKeyPair getIdentityKeyPair(@NonNull Context context) {
    if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");

    try {
      IdentityKey  publicKey  = getIdentityKey(context);
      ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_PREF)));

      return new IdentityKeyPair(publicKey, privateKey);
    } catch (IOException e) {
      throw new AssertionError(e);
    }
  }


  public static void generateIdentityKeys(Context context) {
    IdentityKeyPair identityKeyPair = generateIdentityKeyPair();

    save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(identityKeyPair.getPublicKey().serialize()));
    save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(identityKeyPair.getPrivateKey().serialize()));
  }
  
  public static IdentityKeyPair generateIdentityKeyPair() {
  	//生成公私钥对
    ECKeyPair    djbKeyPair     = Curve.generateKeyPair();
    //PublicKey,包装成IdentityKey
    IdentityKey  djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
    //PrivateKey
    ECPrivateKey djbPrivateKey  = djbKeyPair.getPrivateKey();

    return new IdentityKeyPair(djbIdentityKey, djbPrivateKey);
  }

 ...
 

generateIdentityKeys()方法是生成并保存PrivateKey和PublicKey的,getIdentityKeyPair()方法是获取保存的PrivateKey和PublicKey的。

generateIdentityKeys()方法在哪里调用?
这个方法在PassphraseCreateActivity创建的时候会调用,PassphraseCreateActivity就是app启动时的logo显示页面:

public class PassphraseCreateActivity extends PassphraseActivity {

  public PassphraseCreateActivity() { }

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.create_passphrase_activity);

    initializeResources();
  }

  private void initializeResources() {
    new SecretGenerator().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
  }

  private class SecretGenerator extends AsyncTask<String, Void, Void> {
    private MasterSecret   masterSecret;

    @Override
    protected void onPreExecute() {
    }

    @Override
    protected Void doInBackground(String... params) {
      String passphrase = params[0];
      masterSecret      = MasterSecretUtil.generateMasterSecret(PassphraseCreateActivity.this,
                                                                passphrase);

      MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret);
      IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this);
      VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);

      return null;
    }

    @Override
    protected void onPostExecute(Void param) {
      setMasterSecret(masterSecret);
    }
  }

  @Override
  protected void cleanup() {
    System.gc();
  }
}

这个PassphraseCreateActivity初始化的时候会创建一个AsyncTask调用 IdentityKeyUtil.generateIdentityKeys()进行生成公私钥对的操作。

账户登录时的验证账户的操作

    private static void onAccountVerfied(Context context,
                                         CidRegisterResponse response,
                                         ProfileKey profileKey,
                                         String password,
                                         boolean signUp,
                                         Credentials credentials,
                                         @Nullable String fcmToken) {
        if(response != null &&  response.getCids() != null &&  response.getCids().size()>0)  {
            cid = response.getCids().get(0);
        }else {
            cid = "";
        }
        UUID    uuid   = UuidUtil.parseOrNull(signUp?response.getUuid():response.getLoginToken());

        IdentityKeyPair identityKey  = IdentityKeyUtil.getIdentityKeyPair(context);
        List<PreKeyRecord> records      = PreKeyUtil.generatePreKeys(context);
        //signedPreKey是对公私钥对进行签名后的数据
        SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true);
        boolean                     hasFcm             = fcmToken != null;

        if (signUp) {
            SignalServiceAccountManager accountManager = AccountManagerFactory.createAuthenticated(context, uuid, credentials.getE164number(), credentials.getPassword());
            try {
            	//向服务端注册公钥,即 调用"/v2/keys/%s" 接口
                accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records);
                if (hasFcm) {
                    accountManager.setGcmId(Optional.fromNullable(fcmToken));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }


...

总结,app启动时的logo页面时就会生成公私钥对,然后账户登录时的验证账户的操作会向服务端注册公钥。

发送消息时的加密过程使用到公钥

发送消息时的加密过程会调用ECPublicKey theirIdentity = signalProtocolStore.getIdentity(destinationAddress).getPublicKey();获取保存的ECPublicKey进行加密:

//SealedSessionCipher.java

  public byte[] encrypt(SignalProtocolAddress destinationAddress, SenderCertificate senderCertificate, byte[] paddedPlaintext)
      throws InvalidKeyException, UntrustedIdentityException
  {
    CiphertextMessage message       = new SessionCipher(signalProtocolStore, destinationAddress).encrypt(paddedPlaintext);
    IdentityKeyPair   ourIdentity   = signalProtocolStore.getIdentityKeyPair();
    ECPublicKey       theirIdentity = signalProtocolStore.getIdentity(destinationAddress).getPublicKey();

    ECKeyPair     ephemeral           = Curve.generateKeyPair();
    byte[]        ephemeralSalt       = ByteUtil.combine("UnidentifiedDelivery".getBytes(), theirIdentity.serialize(), ephemeral.getPublicKey().serialize());
    EphemeralKeys ephemeralKeys       = calculateEphemeralKeys(theirIdentity, ephemeral.getPrivateKey(), ephemeralSalt);
    byte[]        staticKeyCiphertext = encrypt(ephemeralKeys.cipherKey, ephemeralKeys.macKey, ourIdentity.getPublicKey().getPublicKey().serialize());

    byte[]                           staticSalt   = ByteUtil.combine(ephemeralKeys.chainKey, staticKeyCiphertext);
    StaticKeys                       staticKeys   = calculateStaticKeys(theirIdentity, ourIdentity.getPrivateKey(), staticSalt);
    UnidentifiedSenderMessageContent content      = new UnidentifiedSenderMessageContent(message.getType(), senderCertificate, message.serialize());
    byte[]                           messageBytes = encrypt(staticKeys.cipherKey, staticKeys.macKey, content.getSerialized());

    return new UnidentifiedSenderMessage(ephemeral.getPublicKey(), staticKeyCiphertext, messageBytes).getSerialized();
  }

这个方法在上面的加密过程的分析中已经分析过了。

接收消息时的解密过程使用到私钥

解密过程,会调用IdentityKeyPair ourIdentity = signalProtocolStore.getIdentityKeyPair(); 获取私钥进行解密:

//SealedSessionCipher.java

  public DecryptionResult decrypt(CertificateValidator validator, byte[] ciphertext, long timestamp)
      throws
      InvalidMetadataMessageException, InvalidMetadataVersionException,
      ProtocolInvalidMessageException, ProtocolInvalidKeyException,
      ProtocolNoSessionException, ProtocolLegacyMessageException,
      ProtocolInvalidVersionException, ProtocolDuplicateMessageException,
      ProtocolInvalidKeyIdException, ProtocolUntrustedIdentityException,
      SelfSendException
  {
    UnidentifiedSenderMessageContent content;

    try {
      IdentityKeyPair           ourIdentity    = signalProtocolStore.getIdentityKeyPair();
      UnidentifiedSenderMessage wrapper        = new UnidentifiedSenderMessage(ciphertext);
      byte[]                    ephemeralSalt  = ByteUtil.combine("UnidentifiedDelivery".getBytes(), ourIdentity.getPublicKey().getPublicKey().serialize(), wrapper.getEphemeral().serialize());
      EphemeralKeys             ephemeralKeys  = calculateEphemeralKeys(wrapper.getEphemeral(), ourIdentity.getPrivateKey(), ephemeralSalt);
      byte[]                    staticKeyBytes = decrypt(ephemeralKeys.cipherKey, ephemeralKeys.macKey, wrapper.getEncryptedStatic());

      ECPublicKey staticKey    = Curve.decodePoint(staticKeyBytes, 0);
      byte[]      staticSalt   = ByteUtil.combine(ephemeralKeys.chainKey, wrapper.getEncryptedStatic());
      StaticKeys  staticKeys   = calculateStaticKeys(staticKey, ourIdentity.getPrivateKey(), staticSalt);
      byte[]      messageBytes = decrypt(staticKeys.cipherKey, staticKeys.macKey, wrapper.getEncryptedMessage());

      content = new UnidentifiedSenderMessageContent(messageBytes);
      validator.validate(content.getSenderCertificate(), timestamp);

      if (!MessageDigest.isEqual(content.getSenderCertificate().getKey().serialize(), staticKeyBytes)) {
        throw new InvalidKeyException("Sender's certificate key does not match key used in message");
      }

      boolean isLocalE164 = localE164Address != null && localE164Address.equals(content.getSenderCertificate().getSenderE164().orNull());
      boolean isLocalUuid = localUuidAddress != null && localUuidAddress.equals(content.getSenderCertificate().getSenderUuid().orNull());

      if ((isLocalE164 || isLocalUuid) && content.getSenderCertificate().getSenderDeviceId() == localDeviceId) {
        throw new SelfSendException();
      }
    } catch (InvalidKeyException | InvalidMacException | InvalidCertificateException e) {
      throw new InvalidMetadataMessageException(e);
    }

    try {
      return new DecryptionResult(content.getSenderCertificate().getSenderUuid(),
                                  content.getSenderCertificate().getSenderE164(),
                                  content.getSenderCertificate().getSenderDeviceId(),
                                  decrypt(content));
    } catch (InvalidMessageException e) {
      throw new ProtocolInvalidMessageException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (InvalidKeyException e) {
      throw new ProtocolInvalidKeyException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (NoSessionException e) {
      throw new ProtocolNoSessionException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (LegacyMessageException e) {
      throw new ProtocolLegacyMessageException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (InvalidVersionException e) {
      throw new ProtocolInvalidVersionException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (DuplicateMessageException e) {
      throw new ProtocolDuplicateMessageException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (InvalidKeyIdException e) {
      throw new ProtocolInvalidKeyIdException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    } catch (UntrustedIdentityException e) {
      throw new ProtocolUntrustedIdentityException(e, content.getSenderCertificate().getSender(), content.getSenderCertificate().getSenderDeviceId());
    }
  }

TextSecureIdentityKeyStore

//TextSecureIdentityKeyStore.java

public class TextSecureIdentityKeyStore implements IdentityKeyStore {

  private static final int TIMESTAMP_THRESHOLD_SECONDS = 5;

  private static final String TAG = Log.tag(TextSecureIdentityKeyStore.class);
  private static final Object LOCK = new Object();

  private final Context context;

  public TextSecureIdentityKeyStore(Context context) {
    this.context = context;
  }

  @Override
  public IdentityKeyPair getIdentityKeyPair() {
    return IdentityKeyUtil.getIdentityKeyPair(context);
  }

...

测试1次非对称加密和500次非对称加密的时间消耗的区别

加密一次:
cipher spend time=8 ms

群聊的测试数据:
加密500次
cipher spend time=2264 ms
cipher spend time=2081 ms
cipher spend time=2008 ms

加密1000次
cipher spend time=4342 ms
cipher spend time=4196 ms
cipher spend time=4107 ms
cipher spend time=4752 ms

以上的测试数据是在单个发送线程中执行加密500/1000次的情况,如果分为多线程执行加密500/1000次的情况下,时间应该会更短。

即优化前的原方案:
500人的群,发送一次群聊消息的花费的时间是:2-3秒 + 500次消息在网络中发送的时间(所以可能出现一次群聊消息需要十几分钟的情况出现)

优化后的新方案:
500人的群,发送一次群聊消息的花费的时间是:2-3秒 + 1次消息在网络中发送的时间

参考:
Java加密之AES/CBC/PKCS5Padding
AES加密算法之AES/CBC/PKCS5Padding
JAVA AES CBC PKCS5Padding加解密
Java实现AES ECP PKCS5Padding加解密工具类

The Public-Key Cryptography Standards (PKCS)

PKCS7 / PKCS5 填充算法

填充模式:PKCS#5/PKCS7
PKCS#1、PKCS#5、PKCS#7区别

https://github.com/signalapp/libsignal-protocol-java
whatsapp协议简单分析之-端对端加密

HMACSHA256原理解析

DH密钥交换和ECDH原理

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值