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();
}
-
先调用SessionCipher的encrypt()方法进行加密,下面的路径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);
}
- 对消息数据进行加密
再看下路径 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消息并使用它来建立会话。
- KeyExchangeMessages。两个客户端可以交换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)
填充模式:PKCS#5/PKCS7
PKCS#1、PKCS#5、PKCS#7区别
https://github.com/signalapp/libsignal-protocol-java
whatsapp协议简单分析之-端对端加密