聊天-服务端验签,发送者验签

会话列表页面

MainActivity的onCreate()执行navigator.onCreate(savedInstanceState);加载ConversationListFragment

ConversationListFragment的RecyclerView list显示会话列表,onItemClick()的handleCreateConversation()进入ConversationActivity

进入ConversationActivity,sendButton发送消息按钮,SendButtonListener -> sendMessage() -> sendTextMessage() -> AsyncTask#doInBackground() -> MessageSender.send() -> sendTextMessage() -> sendSms() -> MmsSendJob.enqueue()

底部的页面是Megaphone

PassphraseRequiredActionBarActivity#onCreate()会调用routeApplicationState()进行页面路由,判断是否要跳转到其他页面,比如PIN设置页面

会话列表的数据来源自哪里?

ApplicationContext#onCreate()调用 initializeMessageRetrieval();创建一个IncomingMessageObserver对象,IncomingMessageObserver的构造方法中开启一个MessageRetrievalThread线程,
MessageRetrievalThread线程会通过SignalServiceMessageReceiver读取服务器的消息,SignalServiceMessageReceiver#createMessagePipe()会创建WebSocketConnection,WebSocketConnection维护着一个webSocket

IncomingMessageProcessor是收到的消息的统一处理入口类,收到的消息会调用IncomingMessageProcessor的processEnvelope()进行处理消息,如果是发送消息的回执则调用processReceipt(),
如果是收到新消息则调用processMessage(),processMessage()会新建PushDecryptMessageJob进行处理消息,PushDecryptMessageJob的onRun()调用handleMessage()

当开始在输入框进行输入时会执行TypingSendJob#onRun(),对方可以看到输入状态

发送消息:

聊天会话页面ConversationActivity,发送按钮是sendButton, SendButtonListener,会调用sendMessage() -> sendTextMessage() -> MessageSender#send(),send()会调用sendTextMessage()

 private static void sendTextMessage(Context context, Recipient recipient,
                                      boolean forceSms, boolean keyExchange,
                                      long messageId)
  {
    if (isLocalSelfSend(context, recipient, forceSms)) {
      sendLocalTextSelf(context, messageId);
    } else if (!forceSms && isPushTextSend(context, recipient, keyExchange)) {
      sendTextPush(recipient, messageId);
    } else {
      sendSms(context, recipient, messageId);
    }
  }

发送普通文本消息是调用sendTextPush():

  private static void sendTextPush(Recipient recipient, long messageId) {
    JobManager jobManager = ApplicationDependencies.getJobManager();
    jobManager.add(new PushTextSendJob(messageId, recipient));
  }

创建一个PushTextSendJob任务放入线程池进行发送消息,任务执行的时候调用基类SendJob#onRun(),调用PushSendJob#onPushSend,最终会调用实现类PushTextSendJob#onPushSend()

onPushSend()会调用deliver()发送消息,deliver()会先调用rotateSenderCertificateIfNecessary()进行验证发送者的证书,验证通过后才调用messageSender.sendMessage()进行真正的发送消息

rotateSenderCertificateIfNecessary():会去SharedPreferences读取key为pref_unidentified_access_certificate_uuid的value,
这个key的value是在RotateCertificateJob#onRun()设置的,

//RotateCertificateJob
  @Override
  public void onRun() throws IOException {
    if (!TextSecurePreferences.isPushRegistered(context)) {
      Log.w(TAG, "Not yet registered. Ignoring.");
      return;
    }

    synchronized (RotateCertificateJob.class) {
      SignalServiceAccountManager accountManager    = ApplicationDependencies.getSignalServiceAccountManager();
      byte[]                      certificate       = accountManager.getSenderCertificate();
      byte[]                      legacyCertificate = accountManager.getSenderCertificateLegacy();

      TextSecurePreferences.setUnidentifiedAccessCertificate(context, certificate);
      TextSecurePreferences.setUnidentifiedAccessCertificateLegacy(context, legacyCertificate);
    }
  }

通过调用SignalServiceAccountManager.getSenderCertificate()方法请求来的:

//SignalServiceAccountManager.java


  public byte[] getSenderCertificate() throws IOException {
    return this.pushServiceSocket.getSenderCertificate();
  }


  protected void rotateSenderCertificateIfNecessary() throws IOException {
    try {
      byte[] certificateBytes = TextSecurePreferences.getUnidentifiedAccessCertificate(context);

      if (certificateBytes == null) {
        throw new InvalidCertificateException("No certificate was present.");
      }
      //发送消息时的证书SenderCertificate是根据这个证书生成的
      SenderCertificate certificate = new SenderCertificate(certificateBytes);

      if (System.currentTimeMillis() > (certificate.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER)) {
        throw new InvalidCertificateException("Certificate is expired, or close to it. Expires on: " + certificate.getExpiration() + ", currently: " + System.currentTimeMillis());
      }

      Log.d(TAG, "Certificate is valid.");
    } catch (InvalidCertificateException e) {
      Log.w(TAG, "Certificate was invalid at send time. Fetching a new one.", e);
      RotateCertificateJob certificateJob = new RotateCertificateJob(context);
      certificateJob.onRun();
    }
  }

这个证书是访问证书,用途是在发送消息的时候先通过验证证书是否过期,如果证书过期了,则不能发送消息,会提示去重新获取证书:"Certificate was invalid at send time. Fetching a new one."
这个证书和后面的发送消息的证书SenderCertificate的区别:发送消息时的证书SenderCertificate是根据这个证书生成的:new SenderCertificate(certificateBytes);

RotateCertificateJob什么时候执行?
1.在手机号注册时,验证码验证成功即会执行:

//CodeVerificationRequest.java
private static void handleSuccessfulRegistration(@NonNull Context context) {
  JobManager jobManager = ApplicationDependencies.getJobManager();
  jobManager.add(new DirectoryRefreshJob(false));
  jobManager.add(new RotateCertificateJob(context));

  DirectoryRefreshListener.schedule(context);
  RotateSignedPreKeyListener.schedule(context);
}

新建一个RotateCertificateJob执行,向服务器请求证书。

2.在rotateSenderCertificateIfNecessary()可以到看,当验证证书无效时也会新建一个RotateCertificateJob执行,即重新向服务器请求证书:

protected void rotateSenderCertificateIfNecessary() throws IOException {
    try {
  	  ...
          
      Log.d(TAG, "Certificate is valid.");
    } catch (InvalidCertificateException e) {
      Log.w(TAG, "Certificate was invalid at send time. Fetching a new one.", e);
      RotateCertificateJob certificateJob = new RotateCertificateJob(context);
      certificateJob.onRun();
   }

}

即输入手机号和验证码注册成功后,就会去发送请求获取证书,请求下来后保存在本地SharedPreferences,下次发消息是直接读取本地的证书即可。

收到消息后报异常:validCertificateException: Signature failed

    org.signal.libsignal.metadata.InvalidMetadataMessageException: org.signal.libsignal.metadata.certificate.InvalidCertificateException: Signature failed
        at org.signal.libsignal.metadata.SealedSessionCipher.decrypt(SealedSessionCipher.java:124)
        at org.whispersystems.signalservice.api.crypto.SignalServiceCipher.decrypt(SignalServiceCipher.java:190)
        at org.whispersystems.signalservice.api.crypto.SignalServiceCipher.decrypt(SignalServiceCipher.java:138)
        at org.thoughtcrime.securesms.jobs.PushDecryptMessageJob.handleMessage(PushDecryptMessageJob.java:159)
        at org.thoughtcrime.securesms.jobs.PushDecryptMessageJob.onRun(PushDecryptMessageJob.java:113)
        at org.thoughtcrime.securesms.jobs.BaseJob.run(BaseJob.java:25)
        at org.thoughtcrime.securesms.jobmanager.JobRunner.run(JobRunner.java:82)
        at org.thoughtcrime.securesms.jobmanager.JobRunner.run(JobRunner.java:46)
     Caused by: org.signal.libsignal.metadata.certificate.InvalidCertificateException: Signature failed
        at org.signal.libsignal.metadata.certificate.CertificateValidator.validate(CertificateValidator.java:45)
        at org.signal.libsignal.metadata.certificate.CertificateValidator.validate(CertificateValidator.java:27)
        at org.signal.libsignal.metadata.SealedSessionCipher.decrypt(SealedSessionCipher.java:111)
        at org.whispersystems.signalservice.api.crypto.SignalServiceCipher.decrypt(SignalServiceCipher.java:190) 
        at org.whispersystems.signalservice.api.crypto.SignalServiceCipher.decrypt(SignalServiceCipher.java:138) 
        at org.thoughtcrime.securesms.jobs.PushDecryptMessageJob.handleMessage(PushDecryptMessageJob.java:159) 
        at org.thoughtcrime.securesms.jobs.PushDecryptMessageJob.onRun(PushDecryptMessageJob.java:113) 
        at org.thoughtcrime.securesms.jobs.BaseJob.run(BaseJob.java:25) 
        at org.thoughtcrime.securesms.jobmanager.JobRunner.run(JobRunner.java:82) 
        at org.thoughtcrime.securesms.jobmanager.JobRunner.run(JobRunner.java:46) 

收到消息后调用SealedSessionCipher#decrypt()进行解密:
SealedSessionCipher.java是org.whispersystems:signal-client-java:0.5.1 jar包里面的代码:

//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);
      //1.CertificateValidator.validate()进行验证
      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 {
      //2.调用decrypt(UnidentifiedSenderMessageContent message)进行解密
      return new DecryptionResult(content.getSenderCertificate().getSenderUuid(),
                                  content.getSenderCertificate().getSenderE164(),
                                  content.getSenderCertificate().getSenderDeviceId(),
                                  decrypt(content));
    } catch (InvalidMessageException e) {
      

...

步骤:
1.CertificateValidator.validate()进行验证,message里面有发送者的证书,这个证书里面有发送者的公钥、消息(代码里叫证书Certificate)和签名。(私钥对消息进行签名,公钥对签名进行验证)
2.调用decrypt(UnidentifiedSenderMessageContent message)进行解密

验签

CertificateValidator.validate(SenderCertificate certificate, long validationTime)验签过程:

//CertificateValidator.java

  public void validate(SenderCertificate certificate, long validationTime) throws InvalidCertificateException {
    try {
      ServerCertificate serverCertificate = certificate.getSigner();
      //1.先调用validate(serverCertificate)对服务器的证书和签名进行验签
      validate(serverCertificate);
	
	  //2.再对发送者进行验证签名
      if (!Curve.verifySignature(serverCertificate.getKey(), certificate.getCertificate(), certificate.getSignature())) {
        throw new InvalidCertificateException("Signature failed");
      }

      if (validationTime > certificate.getExpiration()) {
        throw new InvalidCertificateException("Certificate is expired");
      }
    } catch (InvalidKeyException e) {
      throw new InvalidCertificateException(e);
    }
  }

CertificateValidator.validate()先调用validate(serverCertificate)对服务器的证书和签名进行验签,服务器的公钥是保存在客户端常量UNIDENTIFIED_SENDER_TRUST_ROOT里的。
再调用Curve.verifySignature(ECPublicKey signingKey, byte[] message, byte[] signature) 对发送者进行验证签名:

//Curve.java,是 org.whispersystems:signal-protocol-java:2.8.1 jar包里面的类

  /**
	ECPublicKey signingKey:发送者的公钥
	byte[] message:发送者的证书
	byte[] signature:发送者的签名
	*/
  public static boolean verifySignature(ECPublicKey signingKey, byte[] message, byte[] signature)
      throws InvalidKeyException
  {
    if (signingKey == null || message == null || signature == null) {
      throw new InvalidKeyException("Values must not be null");
    }

    if (signingKey.getType() == DJB_TYPE) {
      return Curve25519.getInstance(BEST)
                       .verifySignature(((DjbECPublicKey) signingKey).getPublicKey(), message, signature);
    } else {
      throw new InvalidKeyException("Unknown type: " + signingKey.getType());
    }
  }

可以看到是使用Curve25519椭圆曲线算法进行验签。
Curve25519是 org.whispersystems:curve25519-java:0.5.0 jar包里面的类

若Curve.verifySignature()返回false,即表示发送者的证书验证失败:如果验证失败则抛出一个InvalidCertificateException(“Signature failed”),即签名验证失败

解密

上述验签步骤完成之后,调用decrypt(UnidentifiedSenderMessageContent message)进行解密。

//SealedSessionCipher.java

...

      return new DecryptionResult(content.getSenderCertificate().getSenderUuid(),
                                  content.getSenderCertificate().getSenderE164(),
                                  content.getSenderCertificate().getSenderDeviceId(),
                                  decrypt(content));

...


  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());
    }
  }

//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());
      }

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

      callback.handlePlaintext(plaintext);

      sessionStore.storeSession(remoteAddress, sessionRecord);

      return plaintext;
    }
  }


  private byte[] decrypt(SessionRecord sessionRecord, SignalMessage ciphertext)
      throws DuplicateMessageException, LegacyMessageException, InvalidMessageException
  {
    synchronized (SESSION_LOCK) {
      Iterator<SessionState> previousStates = sessionRecord.getPreviousSessionStates().iterator();
      List<Exception>        exceptions     = new LinkedList<>();

      try {
        SessionState sessionState = new SessionState(sessionRecord.getSessionState());
        byte[]       plaintext    = decrypt(sessionState, ciphertext);

        sessionRecord.setState(sessionState);
        return plaintext;
      } catch (InvalidMessageException e) {
        exceptions.add(e);
      }

      while (previousStates.hasNext()) {
        try {
          SessionState promotedState = new SessionState(previousStates.next());
          byte[]       plaintext     = decrypt(promotedState, ciphertext);

          previousStates.remove();
          sessionRecord.promoteState(promotedState);

          return plaintext;
        } catch (InvalidMessageException e) {
          exceptions.add(e);
        }
      }

      throw new InvalidMessageException("No valid sessions.", exceptions);
    }
  }

  private byte[] decrypt(SessionState sessionState, SignalMessage ciphertextMessage)
      throws InvalidMessageException, DuplicateMessageException, LegacyMessageException
  {
    if (!sessionState.hasSenderChain()) {
      throw new InvalidMessageException("Uninitialized session!");
    }

    if (ciphertextMessage.getMessageVersion() != sessionState.getSessionVersion()) {
      throw new InvalidMessageException(String.format("Message version %d, but session version %d",
                                                      ciphertextMessage.getMessageVersion(),
                                                      sessionState.getSessionVersion()));
    }

    ECPublicKey    theirEphemeral    = ciphertextMessage.getSenderRatchetKey();
    int            counter           = ciphertextMessage.getCounter();
    ChainKey       chainKey          = getOrCreateChainKey(sessionState, theirEphemeral);
    MessageKeys    messageKeys       = getOrCreateMessageKeys(sessionState, theirEphemeral,
                                                              chainKey, counter);

    ciphertextMessage.verifyMac(sessionState.getRemoteIdentityKey(),
                                sessionState.getLocalIdentityKey(),
                                messageKeys.getMacKey());

    byte[] plaintext = getPlaintext(messageKeys, ciphertextMessage.getBody());

    sessionState.clearUnacknowledgedPreKeyMessage();

    return plaintext;
  }

一步步调用之后,最终调用getPlaintext方法获取到明文:

//SessionCipher.java

  private byte[] getPlaintext(MessageKeys messageKeys, byte[] cipherText)
      throws InvalidMessageException
  {
    try {
      Cipher cipher = getCipher(Cipher.DECRYPT_MODE, messageKeys.getCipherKey(), messageKeys.getIv());
      return cipher.doFinal(cipherText);
    } catch (IllegalBlockSizeException | BadPaddingException e) {
      throw new InvalidMessageException(e);
    }
  }

SenderCertificate如何生成?

org.whispersystems.signalservice.internal.push.SenderCertificate 与 signal-metadata-java.jar 这个jar包里面的 org.signal.libsignal.metadata.certificate包里面的SenderCertificate的区别?

org.whispersystems.signalservice.internal.push.SenderCertificate是服务器请求下来的证书进行反序列化后的实体类,而org.signal.libsignal.metadata.certificate包里面的的SenderCertificate是根据这个证书生成的,如何生成?通过SignalProtos.SenderCertificate

SignalProtos.SenderCertificate这个SenderCertificate是signal-metadata-java.jar 这个jar包里面的 org.signal.libsignal.metadata包里面的SignalProtos类的静态内部类SenderCertificate:

//SenderCertificate.java

   //里面有发送者的公钥,signer如何生成?在构造方法中创建: new ServerCertificate(certificate.getSigner().toByteArray())
   //ServerCertificate serverCertificate = certificate.getSigner();
   // validate(serverCertificate);
  //Curve.verifySignature(serverCertificate.getKey(), certificate.getCertificate(), certificate.getSignature())会取出signer里面的公钥,
  //即发送者会用私钥对数据进行签名,然后把这段数据和签名以及公钥发送给接收者,接收者会根据这段数据和签名以及公钥进行验签
public class SenderCertificate {

  private final ServerCertificate signer;  //服务器的公钥、证书、签名相关数据
  private final ECPublicKey       key; //发送者的公钥
  private final int               senderDeviceId; //发送者的设备id
  private final Optional<String>  senderUuid; //发送者的uuid
  private final Optional<String>  senderE164; //发送者的手机号
  private final long              expiration;

  private final byte[] serialized;  //就是服务器下载的证书的序列化后的数据
  private final byte[] certificate; //发送者的证书,根据服务器证书生成,wrapper.getCertificate().toByteArray()
  private final byte[] signature;   //发送者的签名,根据服务器证书生成,wrapper.getSignature().toByteArray()

  //serialized就是向服务器请求下来的证书序列化后的数据
  public SenderCertificate(byte[] serialized) throws InvalidCertificateException {
    try {
       //调用SignalProtos.SenderCertificate.parseFrom()把序列化后的数据反序列为SignalProtos.SenderCertificate格式的数据,即wrapper就是服务器请求下来的证书反序列后为SignalProtos.SenderCertificate实体类
      SignalProtos.SenderCertificate wrapper = SignalProtos.SenderCertificate.parseFrom(serialized);

      if (!wrapper.hasSignature() || !wrapper.hasCertificate()) {
        throw new InvalidCertificateException("Missing fields");
      }

      SignalProtos.SenderCertificate.Certificate certificate = SignalProtos.SenderCertificate.Certificate.parseFrom(wrapper.getCertificate());

      if (!certificate.hasSigner()       ||
          !certificate.hasIdentityKey()  ||
          !certificate.hasSenderDevice() ||
          !certificate.hasExpires()      ||
          (!certificate.hasSenderUuid() && !certificate.hasSenderE164()))
      {
        throw new InvalidCertificateException("Missing fields");
      }

      this.signer         = new ServerCertificate(certificate.getSigner().toByteArray());
      this.key            = Curve.decodePoint(certificate.getIdentityKey().toByteArray(), 0);
      this.senderUuid     = certificate.hasSenderUuid() ? Optional.of(certificate.getSenderUuid()) : Optional.<String>absent();
      this.senderE164     = certificate.hasSenderE164() ? Optional.of(certificate.getSenderE164()) : Optional.<String>absent();
      this.senderDeviceId = certificate.getSenderDevice();
      this.expiration     = certificate.getExpires();

      this.serialized  = serialized; //服务器下载的证书的序列化后的数据serialized直接赋值给成员变量this.serialized
      this.certificate = wrapper.getCertificate().toByteArray();
      this.signature   = wrapper.getSignature().toByteArray();

    } catch (InvalidProtocolBufferException | InvalidKeyException e) {
      throw new InvalidCertificateException(e);
    }
  }

参数byte[] serialized 就是服务器下载的证书的序列化后的数据,

公钥如何生成?来自服务器下载的证书ServerCertificate的certificate字段的key字段:

public class ServerCertificate {

  private final int         keyId;
  private final ECPublicKey key; //发送者的公钥

  private final byte[] serialized;
  private final byte[] certificate; //同SenderCertificate的certificate字段
  private final byte[] signature; //同SenderCertificate的signature字段

  public ServerCertificate(byte[] serialized) throws InvalidCertificateException {
    try {
      SignalProtos.ServerCertificate wrapper = SignalProtos.ServerCertificate.parseFrom(serialized);

      if (!wrapper.hasCertificate() || !wrapper.hasSignature()) {
        throw new InvalidCertificateException("Missing fields");
      }

      SignalProtos.ServerCertificate.Certificate certificate = SignalProtos.ServerCertificate.Certificate.parseFrom(wrapper.getCertificate());

      if (!certificate.hasId() || !certificate.hasKey()) {
        throw new InvalidCertificateException("Missing fields");
      }

      this.keyId       = certificate.getId();
      this.key         = Curve.decodePoint(certificate.getKey().toByteArray(), 0);  //公钥如何生成?来自服务器下载的证书ServerCertificate的certificate字段的key字段
      this.serialized  = serialized;
      this.certificate = wrapper.getCertificate().toByteArray();
      this.signature   = wrapper.getSignature().toByteArray();

    } catch (InvalidProtocolBufferException | InvalidKeyException e) {
      throw new InvalidCertificateException(e);
    }
  }


  protected void rotateSenderCertificateIfNecessary() throws IOException {
    try {
      byte[] certificateBytes = TextSecurePreferences.getUnidentifiedAccessCertificate(context);

      if (certificateBytes == null) {
        throw new InvalidCertificateException("No certificate was present.");
      }

      //发送消息时的证书SenderCertificate是根据certificateBytes这个证书生成的
      SenderCertificate certificate = new SenderCertificate(certificateBytes);

每次发送消息时都会带上SenderCertificate相关数据:

//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();
  }

注意参数:
SignalProtocolAddress destinationAddress, //接收端地址
SenderCertificate senderCertificate, //发送者的签名相关数据
byte[] paddedPlaintext //要发送的消息数据

接收端接收到消息后会根据签名相关数据进行验签。

总结:

  • 当收到一个消息时先进行验签,验签分两步:(1)对服务器进行验签;(2)对消息发送者进行验签
  • 然后对消息进行解密
  • ui展示

服务端的公钥和私钥以及签名如何生成?

服务端先随机生成私钥,然后使用命令工具根据私钥生成公钥,再随机生成一段数据作为签名时用的证书,然后使用命令工具根据证书和私钥生成签名。

服务端在开发阶段需要将生成的公钥交给客户端,客户端将公钥硬编码在客户端常量里,然后客户端启动时与服务端建立websocket连接成功后,就会调用请求证书的后台接口,接口返回的数据包括certificate和signature两个字段,将这保存在客户端本地。然后客户端每次收到消息时都会根据硬编码在客户端的公钥以及certificate和signature对服务端进行验签(即验证服务器是真正的服务器),验签通过后再对消息发送者进行验签,两次验签通过后进行解密消息。

多设备登录

如果一个账号在本设备登录,又去新设备登录,则再次回到本设备打开app时会报错:WebSocketConnection连接失败,页面提示Device no longer registered
This is likely because you registered your phone number with Signal on a different device. Tap to re-register.

在这里插入图片描述
同时log报错:

2021-04-02 09:37:57.309 19124-19316/org.thoughtcrime.securesms W/WebSocketConnection: onFailure()
    java.net.ProtocolException: Expected HTTP 101 response but was '403 Forbidden'
        at okhttp3.internal.ws.RealWebSocket.checkResponse(RealWebSocket.java:229)
        at okhttp3.internal.ws.RealWebSocket$2.onResponse(RealWebSocket.java:196)
        at okhttp3.RealCall$AsyncCall.execute(RealCall.java:203)
        at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:923)
2021-04-02 09:37:57.309 19124-19316/org.thoughtcrime.securesms W/ApplicationDependencyPr: onAuthenticationFailure()
2021-04-02 09:37:57.309 19124-19316/org.thoughtcrime.securesms I/WebSocketConnection: onClose()
2021-04-02 09:37:57.309 19124-19316/org.thoughtcrime.securesms W/ApplicationDependencyPr: onDisconnected()
2021-04-02 09:37:57.555 19124-19303/org.thoughtcrime.securesms I/WebSocketConnection: connect()
2021-04-02 09:37:57.557 19124-19303/org.thoughtcrime.securesms I/ApplicationDependencyPr: onConnecting()
2021-04-02 09:37:58.208 19124-19319/org.thoughtcrime.securesms W/WebSocketConnection: onFailure()
    java.net.ProtocolException: Expected HTTP 101 response but was '403 Forbidden'
        at okhttp3.internal.ws.RealWebSocket.checkResponse(RealWebSocket.java:229)
        at okhttp3.internal.ws.RealWebSocket$2.onResponse(RealWebSocket.java:196)
        at okhttp3.RealCall$AsyncCall.execute(RealCall.java:203)
        at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:923)

使用的加密算法

非对称加密:curve25519椭圆曲线算法

版本升级相关

Your version of Signal has expired!
或者
Your version of Signal has expired. You can view your message history but you won’t be able to send or receive messages until you update.

ApplicationContext的onForeground()调用checkBuildExpiration()检测客户端版本是否过期,过期则不能发送消息

接口

账户注册:
https://textsecure-service.whispersystems.org

wss://textsecure-service.whispersystems.org/v1/websocket/

“/v1/messages/%s”;
“/v1/messages/%s/%d”; //收到消息后的应答消息,且收到的消息中没有uuid
“/v1/messages/uuid/%s”; //收到消息后的应答消息,且收到的消息中有uuid

版本

android是 4.66.1版本,是2020.7.10发布的版本
ios版本是 2020.4.23发布的的 3.8.0.34 版本
server是 2020.4.23发布的的 3.21 版本,server在3.21版本之后就停止开源了,因此Android和ios都要使用与这个server版本对应的版本

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值