Re-organize gradle modules.

This commit is contained in:
Greyson Parrelli
2025-12-31 11:56:13 -05:00
committed by jeffrey-signal
parent f4863efb2e
commit e162eb27c7
1444 changed files with 111 additions and 144 deletions

View File

@@ -0,0 +1,292 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
class NetworkResultTest {
@Test
fun `generic success`() {
val result = NetworkResult.fromFetch {}
assertTrue(result is NetworkResult.Success)
}
@Test
fun `generic non-successful status code`() {
val exception = NonSuccessfulResponseCodeException(404, "not found", "body")
val result = NetworkResult.fromFetch {
throw exception
}
check(result is NetworkResult.StatusCodeError)
assertEquals(exception, result.exception)
assertEquals(404, result.code)
assertEquals("body", result.stringBody)
}
@Test
fun `generic network error`() {
val exception = PushNetworkException("general exception")
val result = NetworkResult.fromFetch {
throw exception
}
assertTrue(result is NetworkResult.NetworkError)
assertEquals(exception, (result as NetworkResult.NetworkError).exception)
}
@Test
fun `generic application error`() {
val throwable = RuntimeException("test")
val result = NetworkResult.fromFetch {
throw throwable
}
assertTrue(result is NetworkResult.ApplicationError)
assertEquals(throwable, (result as NetworkResult.ApplicationError).throwable)
}
@Test
fun `then - generic`() {
val result = NetworkResult
.fromFetch { NetworkResult.Success(1) }
.then { NetworkResult.Success(2) }
assertTrue(result is NetworkResult.Success)
assertEquals(2, (result as NetworkResult.Success).result)
}
@Test
fun `then - doesn't run on error`() {
val throwable = RuntimeException("test")
var run = false
val result = NetworkResult
.fromFetch { throw throwable }
.then {
run = true
NetworkResult.Success(1)
}
assertTrue(result is NetworkResult.ApplicationError)
assertFalse(run)
}
@Test
fun `map - generic`() {
val result = NetworkResult
.fromFetch { NetworkResult.Success(1) }
.map { 2 }
assertTrue(result is NetworkResult.Success)
assertEquals(2, (result as NetworkResult.Success).result)
}
@Test
fun `map - doesn't run on error`() {
val throwable = RuntimeException("test")
var run = false
val result = NetworkResult
.fromFetch { throw throwable }
.map {
run = true
1
}
assertTrue(result is NetworkResult.ApplicationError)
assertFalse(run)
}
@Test
fun `runIfSuccessful - doesn't run on error`() {
val throwable = RuntimeException("test")
var run = false
val result = NetworkResult
.fromFetch { throw throwable }
.runIfSuccessful { run = true }
assertTrue(result is NetworkResult.ApplicationError)
assertFalse(run)
}
@Test
fun `runIfSuccessful - runs on success`() {
var run = false
NetworkResult
.fromFetch { NetworkResult.Success(1) }
.runIfSuccessful { run = true }
assertTrue(run)
}
@Test
fun `runIfSuccessful - runs before error`() {
val throwable = RuntimeException("test")
var run = false
val result = NetworkResult
.fromFetch { NetworkResult.Success(Unit) }
.runIfSuccessful { run = true }
.then { NetworkResult.ApplicationError<Unit>(throwable) }
assertTrue(result is NetworkResult.ApplicationError)
assertTrue(run)
}
@Test
fun `runOnStatusCodeError - simple call`() {
var handled = false
NetworkResult
.fromFetch { throw NonSuccessfulResponseCodeException(404, "not found", "body") }
.runOnStatusCodeError { handled = true }
assertTrue(handled)
}
@Test
fun `runOnStatusCodeError - ensure only called once`() {
var handleCount = 0
NetworkResult
.fromFetch { throw NonSuccessfulResponseCodeException(404, "not found", "body") }
.runOnStatusCodeError { handleCount++ }
.map { 1 }
.then { NetworkResult.Success(2) }
.map { 3 }
assertEquals(1, handleCount)
}
@Test
fun `runOnStatusCodeError - called when placed before a failing then`() {
var handled = false
val result = NetworkResult
.fromFetch { }
.runOnStatusCodeError { handled = true }
.then { NetworkResult.fromFetch { throw NonSuccessfulResponseCodeException(404, "not found", "body") } }
assertTrue(handled)
assertTrue(result is NetworkResult.StatusCodeError)
}
@Test
fun `runOnStatusCodeError - called when placed two spots before a failing then`() {
var handled = false
val result = NetworkResult
.fromFetch { }
.runOnStatusCodeError { handled = true }
.then { NetworkResult.Success(Unit) }
.then { NetworkResult.fromFetch { throw NonSuccessfulResponseCodeException(404, "not found", "body") } }
assertTrue(handled)
assertTrue(result is NetworkResult.StatusCodeError)
}
@Test
fun `runOnStatusCodeError - should not be called for successful results`() {
var handled = false
NetworkResult
.fromFetch {}
.runOnStatusCodeError { handled = true }
NetworkResult
.fromFetch { throw RuntimeException("application error") }
.runOnStatusCodeError { handled = true }
NetworkResult
.fromFetch { throw PushNetworkException("network error") }
.runOnStatusCodeError { handled = true }
assertFalse(handled)
}
@Test
fun `runOnApplicationError - simple call`() {
var handled = false
NetworkResult
.fromFetch { throw RuntimeException() }
.runOnApplicationError { handled = true }
assertTrue(handled)
}
@Test
fun `runOnApplicationError - ensure only called once`() {
var handleCount = 0
NetworkResult
.fromFetch { throw RuntimeException() }
.runOnApplicationError { handleCount++ }
.map { 1 }
.then { NetworkResult.Success(2) }
.map { 3 }
assertEquals(1, handleCount)
}
@Test
fun `runOnApplicationError - called when placed before a failing then`() {
var handled = false
val result = NetworkResult
.fromFetch { }
.runOnApplicationError { handled = true }
.then { NetworkResult.fromFetch { throw RuntimeException() } }
assertTrue(handled)
assertTrue(result is NetworkResult.ApplicationError)
}
@Test
fun `runOnApplicationError - called when placed two spots before a failing then`() {
var handled = false
val result = NetworkResult
.fromFetch { }
.runOnApplicationError { handled = true }
.then { NetworkResult.Success(Unit) }
.then { NetworkResult.fromFetch { throw RuntimeException() } }
assertTrue(handled)
assertTrue(result is NetworkResult.ApplicationError)
}
@Test
fun `runOnApplicationError - should not be called for successful results`() {
var handled = false
NetworkResult
.fromFetch {}
.runOnApplicationError { handled = true }
NetworkResult
.fromFetch { throw NonSuccessfulResponseCodeException(404, "not found", "body") }
.runOnApplicationError { handled = true }
NetworkResult
.fromFetch { throw PushNetworkException("network error") }
.runOnApplicationError { handled = true }
assertFalse(handled)
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.crypto
import org.junit.Assert.assertEquals
import org.junit.Test
import org.signal.core.util.copyTo
import org.whispersystems.signalservice.internal.util.Util
import java.io.ByteArrayOutputStream
class AttachmentCipherStreamUtilTest {
@Test
fun `getCiphertextLength should return the correct length`() {
for (length in 0..1024) {
val plaintext = ByteArray(length).also { it.fill(0x42) }
val key = Util.getSecretBytes(64)
val iv = Util.getSecretBytes(16)
val outputStream = ByteArrayOutputStream()
val cipherStream = AttachmentCipherOutputStream(key, iv, outputStream)
plaintext.inputStream().copyTo(cipherStream)
val expected = AttachmentCipherStreamUtil.getCiphertextLength(length.toLong())
val actual = outputStream.size().toLong()
assertEquals(expected, actual)
}
}
}

View File

@@ -0,0 +1,954 @@
package org.whispersystems.signalservice.api.crypto
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isTrue
import assertk.fail
import org.conscrypt.Conscrypt
import org.junit.Assert
import org.junit.Ignore
import org.junit.Test
import org.signal.core.util.StreamUtil
import org.signal.core.util.readFully
import org.signal.libsignal.protocol.InvalidMessageException
import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice
import org.signal.libsignal.protocol.incrementalmac.InvalidMacException
import org.signal.libsignal.protocol.kdf.HKDF
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream.IntegrityCheck
import org.whispersystems.signalservice.api.crypto.AttachmentCipherTestHelper.createMediaKeyMaterial
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
import org.whispersystems.signalservice.internal.push.http.AttachmentCipherOutputStreamFactory
import org.whispersystems.signalservice.internal.util.Util
import org.whispersystems.signalservice.testutil.LibSignalLibraryUtil
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.lang.AssertionError
import java.security.DigestInputStream
import java.security.MessageDigest
import java.security.Security
import java.util.Random
class AttachmentCipherTest {
@Test
fun attachment_encryptDecrypt_nonIncremental_encryptedDigest() {
attachment_encryptDecrypt(incremental = false, fileSize = MEBIBYTE, integrityCheckMode = IntegrityCheckMode.ENCRYPTED_DIGEST)
}
@Test
fun attachment_encryptDecrypt_nonIncremental_plaintextHash() {
attachment_encryptDecrypt(incremental = false, fileSize = MEBIBYTE, integrityCheckMode = IntegrityCheckMode.PLAINTEXT_HASH)
}
@Test
fun attachment_encryptDecrypt_nonIncremental_bothIntegrityChecks() {
attachment_encryptDecrypt(incremental = false, fileSize = MEBIBYTE, integrityCheckMode = IntegrityCheckMode.BOTH)
}
@Test
fun attachment_encryptDecrypt_incremental_encryptedDigest() {
attachment_encryptDecrypt(incremental = true, fileSize = MEBIBYTE, integrityCheckMode = IntegrityCheckMode.ENCRYPTED_DIGEST)
}
@Test
fun attachment_encryptDecrypt_incremental_plaintextHash() {
attachment_encryptDecrypt(incremental = true, fileSize = MEBIBYTE, integrityCheckMode = IntegrityCheckMode.PLAINTEXT_HASH)
}
@Test
fun attachment_encryptDecrypt_incremental_bothIntegrityChecks() {
attachment_encryptDecrypt(incremental = true, fileSize = MEBIBYTE, integrityCheckMode = IntegrityCheckMode.BOTH)
}
@Ignore("Useful when making changes, otherwise a bit slow.")
@Test
fun attachment_encryptDecrypt_nonIncremental_manyFileSizes() {
for (i in 0..99) {
attachment_encryptDecrypt(incremental = false, fileSize = MEBIBYTE + Random().nextInt(1, 64 * 1024), integrityCheckMode = IntegrityCheckMode.BOTH)
}
}
@Ignore("Useful when making changes, otherwise a bit slow.")
@Test
fun attachment_encryptDecrypt_incremental_manyFileSizes() {
// Designed to stress the various boundary conditions of reading the final mac
for (i in 0..99) {
attachment_encryptDecrypt(incremental = true, fileSize = MEBIBYTE + Random().nextInt(1, 64 * 1024), IntegrityCheckMode.BOTH)
}
}
private fun attachment_encryptDecrypt(incremental: Boolean, fileSize: Int, integrityCheckMode: IntegrityCheckMode) {
val key = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(fileSize)
val plaintextHash = MessageDigest.getInstance("SHA-256").digest(plaintextInput)
val encryptResult = encryptData(plaintextInput, key, incremental)
val cipherFile = writeToFile(encryptResult.ciphertext)
val integrityCheck = when (integrityCheckMode) {
IntegrityCheckMode.ENCRYPTED_DIGEST -> IntegrityCheck.forEncryptedDigest(encryptResult.digest)
IntegrityCheckMode.PLAINTEXT_HASH -> IntegrityCheck.forPlaintextHash(plaintextHash)
IntegrityCheckMode.BOTH -> IntegrityCheck(
encryptedDigest = encryptResult.digest,
plaintextHash = plaintextHash
)
}
val inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, integrityCheck, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice)
val plaintextOutput = inputStream.readFully()
assertThat(plaintextOutput).isEqualTo(plaintextInput)
cipherFile.delete()
}
@Ignore("Useful when making changes, otherwise a bit slow.")
@Test
fun attachment_encryptDecrypt_skipAll_manyFileSizes() {
for (i in 0..99) {
attachment_encryptDecrypt_skipAll(incremental = false, fileSize = MEBIBYTE + Random().nextInt(1, 64 * 1024))
}
}
private fun attachment_encryptDecrypt_skipAll(incremental: Boolean, fileSize: Int) {
val key = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(fileSize)
val plaintextHash = MessageDigest.getInstance("SHA-256").digest(plaintextInput)
val encryptResult = encryptData(plaintextInput, key, incremental)
val cipherFile = writeToFile(encryptResult.ciphertext)
val integrityCheck = IntegrityCheck(
encryptedDigest = encryptResult.digest,
plaintextHash = plaintextHash
)
val inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, integrityCheck, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice)
while (inputStream.skip(cipherFile.length()) > 0) {
// Empty body, just skipping
}
cipherFile.delete()
}
private fun attachment_encryptDecrypt_plaintextHash(incremental: Boolean, fileSize: Int) {
val key = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(fileSize)
val plaintextHash = MessageDigest.getInstance("SHA-256").digest(plaintextInput)
val encryptResult = encryptData(plaintextInput, key, incremental)
val cipherFile = writeToFile(encryptResult.ciphertext)
val integrityCheck = IntegrityCheck.forPlaintextHash(plaintextHash)
val inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, integrityCheck, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice)
val plaintextOutput = inputStream.readFully()
assertThat(plaintextOutput).isEqualTo(plaintextInput)
cipherFile.delete()
}
@Test
fun attachment_encryptDecryptEmpty_nonIncremental() {
attachment_encryptDecryptEmpty(incremental = false)
}
@Test
fun attachment_encryptDecryptEmpty_incremental() {
attachment_encryptDecryptEmpty(incremental = true)
}
private fun attachment_encryptDecryptEmpty(incremental: Boolean) {
val key = Util.getSecretBytes(64)
val plaintextInput = "".toByteArray()
val encryptResult = encryptData(plaintextInput, key, incremental)
val cipherFile = writeToFile(encryptResult.ciphertext)
val integrityCheck = IntegrityCheck.forEncryptedDigest(encryptResult.digest)
val inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, integrityCheck, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice)
val plaintextOutput = inputStream.readFully()
Assert.assertArrayEquals(plaintextInput, plaintextOutput)
cipherFile.delete()
}
@Test(expected = InvalidMessageException::class)
fun attachment_decryptFailOnBadKey_nonIncremental() {
attachment_decryptFailOnBadKey(incremental = false)
}
@Test(expected = InvalidMessageException::class)
fun attachment_decryptFailOnBadKey_incremental() {
attachment_decryptFailOnBadKey(incremental = true)
}
private fun attachment_decryptFailOnBadKey(incremental: Boolean) {
var cipherFile: File? = null
try {
val key = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val encryptResult = encryptData(plaintextInput, key, incremental)
cipherFile = writeToFile(encryptResult.ciphertext)
val badKey = ByteArray(64)
val integrityCheck = IntegrityCheck.forEncryptedDigest(encryptResult.digest)
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), badKey, integrityCheck, null, 0)
} finally {
cipherFile?.delete()
}
}
@Test(expected = InvalidMessageException::class)
fun attachment_decryptFailOnBadMac_nonIncremental() {
attachment_decryptFailOnBadMac(incremental = false)
}
@Test(expected = InvalidMessageException::class)
fun attachment_decryptFailOnBadMac_incremental() {
attachment_decryptFailOnBadMac(incremental = true)
}
private fun attachment_decryptFailOnBadMac(incremental: Boolean) {
var cipherFile: File? = null
try {
val key = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val encryptResult = encryptData(plaintextInput, key, incremental)
val badMacCiphertext = encryptResult.ciphertext.copyOf(encryptResult.ciphertext.size)
badMacCiphertext[badMacCiphertext.size - 1] = (badMacCiphertext[badMacCiphertext.size - 1] + 1).toByte()
cipherFile = writeToFile(badMacCiphertext)
val integrityCheck = IntegrityCheck.forEncryptedDigest(encryptResult.digest)
val stream: InputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, integrityCheck, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice)
// In incremental mode, we'll only check the digest after reading the whole thing
if (incremental) {
StreamUtil.readFully(stream)
}
} finally {
cipherFile?.delete()
}
}
@Test(expected = InvalidMessageException::class)
fun attachment_decryptFailOnBadEncryptedDigest_nonIncremental() {
attachment_decryptFailOnBadEncryptedDigest(incremental = false)
}
@Test(expected = InvalidMessageException::class)
fun attachment_decryptFailOnBadEncryptedDigest_incremental() {
attachment_decryptFailOnBadEncryptedDigest(incremental = true)
}
@Test(expected = InvalidMessageException::class)
fun attachment_decryptFailOnBadPlaintextHash_nonIncremental() {
attachment_decryptFailOnBadPlaintextHash(incremental = false)
}
@Test(expected = InvalidMessageException::class)
fun attachment_decryptFailOnBadPlaintextHash_incremental() {
attachment_decryptFailOnBadPlaintextHash(incremental = true)
}
private fun attachment_decryptFailOnBadEncryptedDigest(incremental: Boolean) {
var cipherFile: File? = null
try {
val key = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val encryptResult = encryptData(plaintextInput, key, incremental)
val badDigest = ByteArray(32)
cipherFile = writeToFile(encryptResult.ciphertext)
val integrityCheck = IntegrityCheck.forEncryptedDigest(badDigest)
val stream: InputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, integrityCheck, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice)
// In incremental mode, we'll only check the digest after reading the whole thing
if (incremental) {
StreamUtil.readFully(stream)
}
} finally {
cipherFile?.delete()
}
}
private fun attachment_decryptFailOnBadPlaintextHash(incremental: Boolean) {
var cipherFile: File? = null
try {
val key = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val badPlaintextHash = MessageDigest.getInstance("SHA-256").digest(plaintextInput).apply {
this[0] = (this[0] + 1).toByte()
}
val encryptResult = encryptData(plaintextInput, key, incremental)
cipherFile = writeToFile(encryptResult.ciphertext)
val integrityCheck = IntegrityCheck.forPlaintextHash(badPlaintextHash)
val stream: InputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, integrityCheck, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice)
StreamUtil.readFully(stream)
} finally {
cipherFile?.delete()
}
}
@Test
fun attachment_decryptFailOnBadIncrementalDigest() {
var cipherFile: File? = null
var hitCorrectException = false
try {
val key = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val encryptResult = encryptData(plaintextInput, key, true)
val badDigest = Util.getSecretBytes(encryptResult.incrementalDigest.size)
cipherFile = writeToFile(encryptResult.ciphertext)
val integrityCheck = IntegrityCheck.forEncryptedDigest(encryptResult.digest)
val decryptedStream: InputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, integrityCheck, badDigest, encryptResult.chunkSizeChoice)
val plaintextOutput = readInputStreamFully(decryptedStream)
fail(AssertionError("Expected to fail before hitting this line"))
} catch (e: InvalidMacException) {
hitCorrectException = true
} catch (e: InvalidMessageException) {
hitCorrectException = false
} finally {
cipherFile?.delete()
}
assertThat(hitCorrectException).isTrue()
}
@Test
fun archiveInnerAndOuterLayer_encryptDecryptEmpty() {
val innerKey = Util.getSecretBytes(64)
val plaintextInput = "".toByteArray()
val innerEncryptResult = encryptData(plaintextInput, innerKey, withIncremental = false)
val outerKey = Util.getSecretBytes(64)
val outerEncryptResult = encryptData(innerEncryptResult.ciphertext, outerKey, false)
val cipherFile = writeToFile(outerEncryptResult.ciphertext)
val keyMaterial = createMediaKeyMaterial(outerKey)
val decryptedStream = AttachmentCipherInputStream.createForArchivedMedia(
archivedMediaKeyMaterial = keyMaterial,
file = cipherFile,
originalCipherTextLength = innerEncryptResult.ciphertext.size.toLong(),
plaintextLength = plaintextInput.size.toLong(),
combinedKeyMaterial = innerKey,
plaintextHash = innerEncryptResult.plaintextHash,
incrementalDigest = innerEncryptResult.incrementalDigest,
incrementalMacChunkSize = innerEncryptResult.chunkSizeChoice
)
val plaintextOutput = decryptedStream.readFully()
assertThat(plaintextOutput).isEqualTo(plaintextInput)
cipherFile.delete()
}
@Test
fun archiveInnerAndOuterLayer_decryptFailOnBadKey_nonIncremental() {
archiveInnerAndOuterLayer_decryptFailOnBadKey(incremental = false)
}
@Test
fun archiveInnerAndOuterLayer_decryptFailOnBadKey_incremental() {
archiveInnerAndOuterLayer_decryptFailOnBadKey(incremental = true)
}
private fun archiveInnerAndOuterLayer_decryptFailOnBadKey(incremental: Boolean) {
var cipherFile: File? = null
var hitCorrectException = false
try {
val innerKey = Util.getSecretBytes(64)
val badInnerKey = Util.getSecretBytes(64)
val plaintextInput = "Gwen Stacy".toByteArray()
val innerEncryptResult = encryptData(plaintextInput, innerKey, incremental)
val outerKey = Util.getSecretBytes(64)
val outerEncryptResult = encryptData(innerEncryptResult.ciphertext, outerKey, false)
val cipherFile = writeToFile(outerEncryptResult.ciphertext)
val keyMaterial = createMediaKeyMaterial(badInnerKey)
AttachmentCipherInputStream.createForArchivedMedia(
archivedMediaKeyMaterial = keyMaterial,
file = cipherFile,
originalCipherTextLength = innerEncryptResult.ciphertext.size.toLong(),
plaintextLength = plaintextInput.size.toLong(),
combinedKeyMaterial = innerKey,
plaintextHash = innerEncryptResult.digest,
incrementalDigest = innerEncryptResult.incrementalDigest,
incrementalMacChunkSize = innerEncryptResult.chunkSizeChoice
)
} catch (e: InvalidMessageException) {
hitCorrectException = true
} finally {
cipherFile?.delete()
}
assertThat(hitCorrectException).isTrue()
}
@Test
fun archive_encryptDecrypt_nonIncremental() {
archive_encryptDecrypt(incremental = false, fileSize = MEBIBYTE)
}
@Test
fun archive_encryptDecrypt_incremental() {
archive_encryptDecrypt(incremental = true, fileSize = MEBIBYTE)
}
@Ignore("Useful when making changes, otherwise a bit slow.")
@Test
fun archive_encryptDecrypt_nonIncremental_manyFileSizes() {
for (i in 0..99) {
archive_encryptDecrypt(incremental = false, fileSize = MEBIBYTE + Random().nextInt(1, 64 * 1024))
}
}
@Ignore("Useful when making changes, otherwise a bit slow.")
@Test
fun archive_encryptDecrypt_incremental_manyFileSizes() {
// Designed to stress the various boundary conditions of reading the final mac
for (i in 0..99) {
archive_encryptDecrypt(incremental = true, fileSize = MEBIBYTE + Random().nextInt(1, 64 * 1024))
}
}
private fun archive_encryptDecrypt(incremental: Boolean, fileSize: Int) {
val innerKey = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(fileSize)
val innerEncryptResult = encryptData(plaintextInput, innerKey, incremental)
val outerKey = Util.getSecretBytes(64)
val outerEncryptResult = encryptData(innerEncryptResult.ciphertext, outerKey, withIncremental = false, padded = false) // Server doesn't pad
val cipherFile = writeToFile(outerEncryptResult.ciphertext)
val keyMaterial = createMediaKeyMaterial(outerKey)
val decryptedStream = AttachmentCipherInputStream.createForArchivedMedia(
archivedMediaKeyMaterial = keyMaterial,
file = cipherFile,
originalCipherTextLength = innerEncryptResult.ciphertext.size.toLong(),
plaintextLength = plaintextInput.size.toLong(),
combinedKeyMaterial = innerKey,
plaintextHash = innerEncryptResult.plaintextHash,
incrementalDigest = innerEncryptResult.incrementalDigest,
incrementalMacChunkSize = innerEncryptResult.chunkSizeChoice
)
val plaintextOutput = decryptedStream.readFully()
assertThat(plaintextOutput).isEqualTo(plaintextInput)
cipherFile.delete()
}
@Ignore("Useful when making changes, otherwise a bit slow.")
@Test
fun archiveThumbnail_encryptDecrypt_manyFileSizes() {
for (i in 0..99) {
archiveThumbnail_encryptDecrypt(fileSize = MEBIBYTE + Random().nextInt(1, 64 * 1024))
}
}
private fun archiveThumbnail_encryptDecrypt(fileSize: Int) {
val innerKey = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(fileSize)
val innerEncryptResult = encryptData(plaintextInput, innerKey, withIncremental = false)
val outerKey = Util.getSecretBytes(64)
val outerEncryptResult = encryptData(innerEncryptResult.ciphertext, outerKey, withIncremental = false, padded = false) // Server doesn't pad
val cipherFile = writeToFile(outerEncryptResult.ciphertext)
val keyMaterial = createMediaKeyMaterial(outerKey)
val decryptedStream = AttachmentCipherInputStream.createForArchivedThumbnail(
archivedMediaKeyMaterial = keyMaterial,
file = cipherFile,
innerCombinedKeyMaterial = innerKey
)
val plaintextOutput = decryptedStream.readFully()
// We knowingly keep padding on thumbnails, so for the test to work, we strip the padding off, but check to make sure the sizes match up beforehand
assertThat(plaintextOutput.size).isEqualTo(PaddingInputStream.getPaddedSize(plaintextInput.size.toLong()).toInt())
assertThat(plaintextOutput.copyOfRange(fromIndex = 0, toIndex = plaintextInput.size)).isEqualTo(plaintextInput)
cipherFile.delete()
}
@Test
fun archiveEncryptDecrypt_decryptFailOnInnerBadMac() {
var cipherFile: File? = null
var hitCorrectException = false
try {
val innerKey = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val innerEncryptResult = encryptData(plaintextInput, innerKey, withIncremental = true, padded = false) // Server doesn't pad
val badMacInnerCipherText = innerEncryptResult.ciphertext.copyOf().also {
it[it.size - 1] = (it[it.size - 1] + 1).toByte()
}
val outerKey = Util.getSecretBytes(64)
val outerEncryptResult = encryptData(badMacInnerCipherText, outerKey, false)
cipherFile = writeToFile(outerEncryptResult.ciphertext)
val keyMaterial = createMediaKeyMaterial(innerKey)
AttachmentCipherInputStream.createForArchivedMedia(
archivedMediaKeyMaterial = keyMaterial,
file = cipherFile,
originalCipherTextLength = innerEncryptResult.ciphertext.size.toLong(),
plaintextLength = plaintextInput.size.toLong(),
combinedKeyMaterial = innerKey,
plaintextHash = innerEncryptResult.digest,
incrementalDigest = innerEncryptResult.incrementalDigest,
incrementalMacChunkSize = innerEncryptResult.chunkSizeChoice
)
Assert.fail()
} catch (e: InvalidMessageException) {
hitCorrectException = true
} finally {
cipherFile?.delete()
}
Assert.assertTrue(hitCorrectException)
}
@Test
fun archiveEncryptDecrypt_decryptFailOnOuterMac() {
var cipherFile: File? = null
var hitCorrectException = false
try {
val innerKey = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val innerEncryptResult = encryptData(plaintextInput, innerKey, withIncremental = true, padded = false) // Server doesn't pad
val outerKey = Util.getSecretBytes(64)
val outerEncryptResult = encryptData(innerEncryptResult.ciphertext, outerKey, false)
val badMacOuterCiphertext = outerEncryptResult.ciphertext.copyOf().also {
it[it.size - 1] = (it[it.size - 1] + 1).toByte()
}
cipherFile = writeToFile(badMacOuterCiphertext)
val keyMaterial = createMediaKeyMaterial(innerKey)
AttachmentCipherInputStream.createForArchivedMedia(
archivedMediaKeyMaterial = keyMaterial,
file = cipherFile,
originalCipherTextLength = innerEncryptResult.ciphertext.size.toLong(),
plaintextLength = plaintextInput.size.toLong(),
combinedKeyMaterial = innerKey,
plaintextHash = innerEncryptResult.digest,
incrementalDigest = innerEncryptResult.incrementalDigest,
incrementalMacChunkSize = innerEncryptResult.chunkSizeChoice
)
Assert.fail()
} catch (e: InvalidMessageException) {
hitCorrectException = true
} finally {
cipherFile?.delete()
}
Assert.assertTrue(hitCorrectException)
}
@Test
fun archiveThumbnailEncryptDecrypt_decryptFailOnInnerBadMac() {
var cipherFile: File? = null
var hitCorrectException = false
try {
val innerKey = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val innerEncryptResult = encryptData(plaintextInput, innerKey, withIncremental = true, padded = false) // Server doesn't pad
val badMacInnerCipherText = innerEncryptResult.ciphertext.copyOf().also {
it[it.size - 1] = (it[it.size - 1] + 1).toByte()
}
val outerKey = Util.getSecretBytes(64)
val outerEncryptResult = encryptData(badMacInnerCipherText, outerKey, false)
cipherFile = writeToFile(outerEncryptResult.ciphertext)
val keyMaterial = createMediaKeyMaterial(innerKey)
AttachmentCipherInputStream.createForArchivedThumbnail(
archivedMediaKeyMaterial = keyMaterial,
file = cipherFile,
innerCombinedKeyMaterial = innerKey
).readFully()
Assert.fail()
} catch (e: InvalidMessageException) {
hitCorrectException = true
} finally {
cipherFile?.delete()
}
Assert.assertTrue(hitCorrectException)
}
@Test
fun archiveThumbnailEncryptDecrypt_decryptFailOnOuterMac() {
var cipherFile: File? = null
var hitCorrectException = false
try {
val innerKey = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val innerEncryptResult = encryptData(plaintextInput, innerKey, withIncremental = true, padded = false) // Server doesn't pad
val outerKey = Util.getSecretBytes(64)
val outerEncryptResult = encryptData(innerEncryptResult.ciphertext, outerKey, false)
val badMacOuterCiphertext = outerEncryptResult.ciphertext.copyOf().also {
it[it.size - 1] = (it[it.size - 1] + 1).toByte()
}
cipherFile = writeToFile(badMacOuterCiphertext)
val keyMaterial = createMediaKeyMaterial(innerKey)
AttachmentCipherInputStream.createForArchivedThumbnail(
archivedMediaKeyMaterial = keyMaterial,
file = cipherFile,
innerCombinedKeyMaterial = innerKey
)
Assert.fail()
} catch (e: InvalidMessageException) {
hitCorrectException = true
} finally {
cipherFile?.delete()
}
Assert.assertTrue(hitCorrectException)
}
@Test
fun sticker_encryptDecrypt() {
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS()
val packKey = Util.getSecretBytes(32)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val encryptResult = encryptData(plaintextInput, expandPackKey(packKey), withIncremental = false, padded = false)
val inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey)
val plaintextOutput = readInputStreamFully(inputStream)
Assert.assertArrayEquals(plaintextInput, plaintextOutput)
}
@Test
fun sticker_encryptDecryptEmpty() {
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS()
val packKey = Util.getSecretBytes(32)
val plaintextInput = "".toByteArray()
val encryptResult = encryptData(plaintextInput, expandPackKey(packKey), withIncremental = false, padded = false)
val inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey)
val plaintextOutput = readInputStreamFully(inputStream)
Assert.assertArrayEquals(plaintextInput, plaintextOutput)
}
@Test
fun sticker_decryptFailOnBadKey() {
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS()
var hitCorrectException = false
try {
val packKey = Util.getSecretBytes(32)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true)
val badPackKey = ByteArray(32)
AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, badPackKey)
} catch (e: InvalidMessageException) {
hitCorrectException = true
}
Assert.assertTrue(hitCorrectException)
}
@Test
fun sticker_decryptFailOnBadMac() {
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS()
var hitCorrectException = false
try {
val packKey = Util.getSecretBytes(32)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true)
val badMacCiphertext = encryptResult.ciphertext.copyOf(encryptResult.ciphertext.size)
badMacCiphertext[badMacCiphertext.size - 1] = (badMacCiphertext[badMacCiphertext.size - 1] + 1).toByte()
AttachmentCipherInputStream.createForStickerData(badMacCiphertext, packKey)
} catch (e: InvalidMessageException) {
hitCorrectException = true
}
Assert.assertTrue(hitCorrectException)
}
@Test
fun outputStream_writeAfterFlush() {
val key = Util.getSecretBytes(64)
val iv = Util.getSecretBytes(16)
val plaintextInput1 = Util.getSecretBytes(MEBIBYTE)
val plaintextInput2 = Util.getSecretBytes(MEBIBYTE)
val destinationOutputStream = ByteArrayOutputStream()
val encryptingOutputStream = AttachmentCipherOutputStreamFactory(key, iv).createFor(destinationOutputStream)
encryptingOutputStream.write(plaintextInput1)
encryptingOutputStream.flush()
encryptingOutputStream.write(plaintextInput2)
encryptingOutputStream.close()
val encryptedData = destinationOutputStream.toByteArray()
val digest = encryptingOutputStream.transmittedDigest
val combinedData = plaintextInput1 + plaintextInput2
val cipherFile = writeToFile(encryptedData)
val integrityCheck = IntegrityCheck.forEncryptedDigest(digest)
val decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, combinedData.size.toLong(), key, integrityCheck, null, 0)
val plaintextOutput = readInputStreamFully(decryptedStream)
assertThat(plaintextOutput).isEqualTo(combinedData)
cipherFile.delete()
}
@Test
fun outputStream_flushMultipleTimes() {
val key = Util.getSecretBytes(64)
val iv = Util.getSecretBytes(16)
val plaintextInput1 = Util.getSecretBytes(MEBIBYTE)
val plaintextInput2 = Util.getSecretBytes(MEBIBYTE)
val destinationOutputStream = ByteArrayOutputStream()
val encryptingOutputStream = AttachmentCipherOutputStreamFactory(key, iv).createFor(destinationOutputStream)
encryptingOutputStream.write(plaintextInput1)
encryptingOutputStream.flush()
encryptingOutputStream.flush()
encryptingOutputStream.flush()
encryptingOutputStream.write(plaintextInput2)
encryptingOutputStream.close()
val encryptedData = destinationOutputStream.toByteArray()
val digest = encryptingOutputStream.transmittedDigest
val combinedData = plaintextInput1 + plaintextInput2
val cipherFile = writeToFile(encryptedData)
val integrityCheck = IntegrityCheck.forEncryptedDigest(digest)
val decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, combinedData.size.toLong(), key, integrityCheck, null, 0)
val plaintextOutput = readInputStreamFully(decryptedStream)
assertThat(plaintextOutput).isEqualTo(combinedData)
cipherFile.delete()
}
@Test
fun outputStream_singleByteWrite() {
val key = Util.getSecretBytes(64)
val iv = Util.getSecretBytes(16)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val destinationOutputStream = ByteArrayOutputStream()
val encryptingOutputStream = AttachmentCipherOutputStreamFactory(key, iv).createFor(destinationOutputStream)
for (b in plaintextInput) {
encryptingOutputStream.write(b.toInt())
}
encryptingOutputStream.close()
val encryptedData = destinationOutputStream.toByteArray()
val digest = encryptingOutputStream.transmittedDigest
val cipherFile = writeToFile(encryptedData)
val integrityCheck = IntegrityCheck.forEncryptedDigest(digest)
val decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, integrityCheck, null, 0)
val plaintextOutput = readInputStreamFully(decryptedStream)
assertThat(plaintextOutput).isEqualTo(plaintextInput)
cipherFile.delete()
}
@Test
fun outputStream_mixedSingleByteAndArrayWrites() {
val key = Util.getSecretBytes(64)
val iv = Util.getSecretBytes(16)
val plaintextInput1 = Util.getSecretBytes(512)
val plaintextInput2 = Util.getSecretBytes(512)
val destinationOutputStream = ByteArrayOutputStream()
val encryptingOutputStream = AttachmentCipherOutputStreamFactory(key, iv).createFor(destinationOutputStream)
// Write first part as array
encryptingOutputStream.write(plaintextInput1)
// Write second part one byte at a time
for (b in plaintextInput2) {
encryptingOutputStream.write(b.toInt())
}
encryptingOutputStream.close()
val expectedData = plaintextInput1 + plaintextInput2
val encryptedData = destinationOutputStream.toByteArray()
val digest = encryptingOutputStream.transmittedDigest
val cipherFile = writeToFile(encryptedData)
val integrityCheck = IntegrityCheck.forEncryptedDigest(digest)
val decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, expectedData.size.toLong(), key, integrityCheck, null, 0)
val plaintextOutput = readInputStreamFully(decryptedStream)
assertThat(plaintextOutput).isEqualTo(expectedData)
cipherFile.delete()
}
@Test
fun outputStream_singleByteWriteWithFlushes() {
val key = Util.getSecretBytes(64)
val iv = Util.getSecretBytes(16)
val plaintextInput = Util.getSecretBytes(256)
val destinationOutputStream = ByteArrayOutputStream()
val encryptingOutputStream = AttachmentCipherOutputStreamFactory(key, iv).createFor(destinationOutputStream)
// Write bytes one at a time with occasional flushes
for (i in plaintextInput.indices) {
encryptingOutputStream.write(plaintextInput[i].toInt())
if (i % 64 == 0) {
encryptingOutputStream.flush()
}
}
encryptingOutputStream.close()
val encryptedData = destinationOutputStream.toByteArray()
val digest = encryptingOutputStream.transmittedDigest
val cipherFile = writeToFile(encryptedData)
val integrityCheck = IntegrityCheck.forEncryptedDigest(digest)
val decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, integrityCheck, null, 0)
val plaintextOutput = readInputStreamFully(decryptedStream)
assertThat(plaintextOutput).isEqualTo(plaintextInput)
cipherFile.delete()
}
private class EncryptResult(
val ciphertext: ByteArray,
val digest: ByteArray,
val incrementalDigest: ByteArray,
val chunkSizeChoice: Int,
val plaintextHash: ByteArray
)
companion object {
init {
// https://github.com/google/conscrypt/issues/1034
if (System.getProperty("os.arch") != "aarch64") {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
}
}
private const val MEBIBYTE = 1024 * 1024
private fun encryptData(data: ByteArray, keyMaterial: ByteArray, withIncremental: Boolean, padded: Boolean = true): EncryptResult {
val digestingStream = DigestInputStream(ByteArrayInputStream(data), MessageDigest.getInstance("SHA-256"))
val actualData = if (padded) {
PaddingInputStream(digestingStream, data.size.toLong()).readFully()
} else {
digestingStream.readFully()
}
val plaintextHash = digestingStream.messageDigest.digest()
val outputStream = ByteArrayOutputStream()
val incrementalDigestOut = ByteArrayOutputStream()
val iv = Util.getSecretBytes(16)
val factory = AttachmentCipherOutputStreamFactory(keyMaterial, iv)
val encryptStream: DigestingOutputStream
val sizeChoice = ChunkSizeChoice.inferChunkSize(actualData.size)
encryptStream = if (withIncremental) {
factory.createIncrementalFor(outputStream, actualData.size.toLong(), sizeChoice, incrementalDigestOut)
} else {
factory.createFor(outputStream)
}
encryptStream.write(actualData)
encryptStream.flush()
encryptStream.close()
incrementalDigestOut.close()
return EncryptResult(
ciphertext = outputStream.toByteArray(),
digest = encryptStream.transmittedDigest,
incrementalDigest = incrementalDigestOut.toByteArray(),
chunkSizeChoice = sizeChoice.sizeInBytes,
plaintextHash = plaintextHash
)
}
private fun writeToFile(data: ByteArray): File {
val file = File.createTempFile("temp", ".data")
val outputStream: OutputStream = FileOutputStream(file)
outputStream.write(data)
outputStream.close()
return file
}
private fun readInputStreamFully(inputStream: InputStream): ByteArray {
return Util.readFullyAsBytes(inputStream)
}
private fun expandPackKey(shortKey: ByteArray): ByteArray {
return HKDF.deriveSecrets(shortKey, "Sticker Pack".toByteArray(), 64)
}
}
enum class IntegrityCheckMode {
ENCRYPTED_DIGEST,
PLAINTEXT_HASH,
BOTH
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.crypto
import org.signal.core.models.backup.MediaId
import org.signal.core.models.backup.MediaRootBackupKey.MediaKeyMaterial
import org.whispersystems.signalservice.internal.util.Util
object AttachmentCipherTestHelper {
/**
* Needed to workaround this bug:
* https://youtrack.jetbrains.com/issue/KT-60205/Java-class-has-private-access-in-class-constructor-with-inlinevalue-parameter
*/
@JvmStatic
fun createMediaKeyMaterial(combinedKey: ByteArray): MediaKeyMaterial {
val parts = Util.split(combinedKey, 32, 32)
return MediaKeyMaterial(
id = MediaId(Util.getSecretBytes(15)),
macKey = parts[1],
aesKey = parts[0]
)
}
}

View File

@@ -0,0 +1,207 @@
package org.whispersystems.signalservice.api.crypto
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isTrue
import assertk.fail
import org.junit.Test
import org.signal.core.util.readFully
import org.signal.libsignal.protocol.InvalidMessageException
import java.io.ByteArrayInputStream
import java.security.MessageDigest
class DigestValidatingInputStreamTest {
@Test
fun `success - read byte by byte`() {
val data = "Hello, World!".toByteArray()
val digest = MessageDigest.getInstance("SHA-256")
val expectedHash = digest.digest(data)
val inputStream = ByteArrayInputStream(data)
val digestEnforcingStream = DigestValidatingInputStream(
inputStream = inputStream,
digest = MessageDigest.getInstance("SHA-256"),
expectedHash = expectedHash
)
val result = ByteArray(data.size)
var i = 0
var byteRead: Byte = 0
while (digestEnforcingStream.read().also { byteRead = it.toByte() } != -1) {
result[i] = byteRead
i++
}
assertThat(result).isEqualTo(data)
assertThat(digestEnforcingStream.validationAttempted).isTrue()
digestEnforcingStream.close()
}
@Test
fun `success - read byte array`() {
val data = "Hello, World! This is a longer message to test buffer reading.".toByteArray()
val digest = MessageDigest.getInstance("SHA-256")
val expectedHash = digest.digest(data)
val inputStream = ByteArrayInputStream(data)
val digestEnforcingStream = DigestValidatingInputStream(
inputStream = inputStream,
digest = MessageDigest.getInstance("SHA-256"),
expectedHash = expectedHash
)
val result = digestEnforcingStream.readFully()
assertThat(result.size).isEqualTo(data.size)
assertThat(result).isEqualTo(data)
assertThat(digestEnforcingStream.validationAttempted).isTrue()
digestEnforcingStream.close()
}
@Test
fun `success - read byte array with offset and length`() {
val data = "This is test data for offset and length reading.".toByteArray()
val digest = MessageDigest.getInstance("SHA-256")
val expectedHash = digest.digest(data)
val inputStream = ByteArrayInputStream(data)
val digestEnforcingStream = DigestValidatingInputStream(
inputStream = inputStream,
digest = MessageDigest.getInstance("SHA-256"),
expectedHash = expectedHash
)
val buffer = ByteArray(1024)
var totalBytesRead = 0
var bytesRead: Int
while (digestEnforcingStream.read(buffer, totalBytesRead, 10).also { bytesRead = it } > 0) {
totalBytesRead += bytesRead
}
val result = buffer.copyOf(totalBytesRead)
assertThat(result).isEqualTo(data)
assertThat(digestEnforcingStream.validationAttempted).isTrue()
digestEnforcingStream.close()
}
@Test
fun `success - empty data`() {
val data = ByteArray(0)
val digest = MessageDigest.getInstance("SHA-256")
val expectedHash = digest.digest(data)
val inputStream = ByteArrayInputStream(data)
val digestEnforcingStream = DigestValidatingInputStream(
inputStream = inputStream,
digest = MessageDigest.getInstance("SHA-256"),
expectedHash = expectedHash
)
// Should immediately return -1 and validate
val endByte = digestEnforcingStream.read()
assertThat(endByte).isEqualTo(-1)
assertThat(digestEnforcingStream.validationAttempted).isTrue()
digestEnforcingStream.close()
}
@Test
fun `success - alternative digest, md5`() {
val data = "Testing MD5 hash validation".toByteArray()
val digest = MessageDigest.getInstance("MD5")
val expectedHash = digest.digest(data)
val inputStream = ByteArrayInputStream(data)
val digestEnforcingStream = DigestValidatingInputStream(
inputStream = inputStream,
digest = MessageDigest.getInstance("MD5"),
expectedHash = expectedHash
)
val result = digestEnforcingStream.readFully()
assertThat(result).isEqualTo(data)
assertThat(digestEnforcingStream.validationAttempted).isTrue()
digestEnforcingStream.close()
}
@Test
fun `success - multiple reads after close`() {
val data = "Test multiple validation calls".toByteArray()
val digest = MessageDigest.getInstance("SHA-256")
val expectedHash = digest.digest(data)
val inputStream = ByteArrayInputStream(data)
val digestEnforcingStream = DigestValidatingInputStream(
inputStream = inputStream,
digest = MessageDigest.getInstance("SHA-256"),
expectedHash = expectedHash
)
val result = digestEnforcingStream.readFully()
// Multiple calls to read() after EOF should not cause issues
assertThat(digestEnforcingStream.read()).isEqualTo(-1)
assertThat(digestEnforcingStream.read()).isEqualTo(-1)
assertThat(digestEnforcingStream.read()).isEqualTo(-1)
assertThat(result).isEqualTo(data)
assertThat(digestEnforcingStream.validationAttempted).isTrue()
digestEnforcingStream.close()
}
@Test
fun `failure - read byte by byte`() {
val data = "Hello, World!".toByteArray()
val wrongHash = ByteArray(32) // All zeros - wrong hash
val inputStream = ByteArrayInputStream(data)
val digestEnforcingStream = DigestValidatingInputStream(
inputStream = inputStream,
digest = MessageDigest.getInstance("SHA-256"),
expectedHash = wrongHash
)
try {
while (digestEnforcingStream.read() != -1) {
// Reading byte by byte
}
fail("Expected InvalidCiphertextException to be thrown")
} catch (e: InvalidMessageException) {
// Expected exception
} finally {
digestEnforcingStream.close()
}
}
@Test
fun `failure - read byte array`() {
val data = "Hello, World! This is a test message.".toByteArray()
val wrongHash = ByteArray(32) // All zeros - wrong hash
val inputStream = ByteArrayInputStream(data)
val digestEnforcingStream = DigestValidatingInputStream(
inputStream = inputStream,
digest = MessageDigest.getInstance("SHA-256"),
expectedHash = wrongHash
)
try {
digestEnforcingStream.readFully()
fail("Expected InvalidCiphertextException to be thrown")
} catch (e: InvalidMessageException) {
// Expected exception
} finally {
digestEnforcingStream.close()
}
}
}

View File

@@ -0,0 +1,188 @@
package org.whispersystems.signalservice.api.crypto
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isTrue
import assertk.fail
import org.junit.Test
import org.signal.core.util.kibiBytes
import org.signal.core.util.mebiBytes
import org.signal.core.util.readFully
import org.signal.libsignal.protocol.InvalidMessageException
import org.whispersystems.signalservice.internal.util.Util
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.random.Random
class MacValidatingInputStreamTest {
@Test
fun `success - simple byte array read`() {
val data = "Hello, World!".toByteArray()
val key = Util.getSecretBytes(32)
val dataWithMac = createDataWithMac(data, key)
val inputStream = ByteArrayInputStream(dataWithMac)
val mac = createMac(key)
val macValidatingStream = MacValidatingInputStream(inputStream, mac)
val result = macValidatingStream.readFully()
assertThat(result).isEqualTo(dataWithMac)
macValidatingStream.close()
}
@Test
fun `success - byte by byte`() {
val data = "Hello, World!".toByteArray()
val key = Util.getSecretBytes(32)
val dataWithMac = createDataWithMac(data, key)
val inputStream = ByteArrayInputStream(dataWithMac)
val mac = createMac(key)
val macValidatingStream = MacValidatingInputStream(inputStream, mac)
val out = ByteArrayOutputStream()
var read = -1
while (macValidatingStream.read().also { read = it } != -1) {
out.write(read)
}
val result = out.toByteArray()
assertThat(result).isEqualTo(dataWithMac)
macValidatingStream.close()
}
@Test
fun `success - many different sizes`() {
for (i in 1..100) {
val data = Util.getSecretBytes(Random.nextLong(from = 256.kibiBytes.bytes, until = 2.mebiBytes.bytes).toInt())
val key = Util.getSecretBytes(32)
val dataWithMac = createDataWithMac(data, key)
val inputStream = ByteArrayInputStream(dataWithMac)
val mac = createMac(key)
val macValidatingStream = MacValidatingInputStream(inputStream, mac)
val result = macValidatingStream.readFully()
assertThat(result).isEqualTo(dataWithMac)
assertThat(macValidatingStream.validationAttempted).isTrue()
macValidatingStream.close()
}
}
@Test
fun `success - empty data`() {
val data = ByteArray(0)
val key = Util.getSecretBytes(32)
val dataWithMac = createDataWithMac(data, key)
val inputStream = ByteArrayInputStream(dataWithMac)
val mac = createMac(key)
val macValidatingStream = MacValidatingInputStream(inputStream, mac)
val result = macValidatingStream.readFully()
assertThat(result).isEqualTo(dataWithMac)
assertThat(macValidatingStream.validationAttempted).isTrue()
macValidatingStream.close()
}
@Test
fun `success - data exactly MAC length`() {
val key = Util.getSecretBytes(32)
val mac = createMac(key)
val macLength = mac.macLength
val data = ByteArray(macLength) { (it % 256).toByte() } // Data same size as MAC
val dataWithMac = createDataWithMac(data, key)
val inputStream = ByteArrayInputStream(dataWithMac)
val mac2 = createMac(key)
val macValidatingStream = MacValidatingInputStream(inputStream, mac2)
val result = macValidatingStream.readFully()
assertThat(result).isEqualTo(dataWithMac)
assertThat(macValidatingStream.validationAttempted).isTrue()
macValidatingStream.close()
}
@Test
fun `success - multiple reads after end of stream`() {
val data = "Test multiple reads after EOF".toByteArray()
val key = Util.getSecretBytes(32)
val dataWithMac = createDataWithMac(data, key)
val inputStream = ByteArrayInputStream(dataWithMac)
val mac = createMac(key)
val macValidatingStream = MacValidatingInputStream(inputStream, mac)
val result = macValidatingStream.readFully()
// Multiple calls to read() after EOF should return -1
assertThat(macValidatingStream.read()).isEqualTo(-1)
assertThat(macValidatingStream.read()).isEqualTo(-1)
assertThat(macValidatingStream.read()).isEqualTo(-1)
assertThat(result).isEqualTo(dataWithMac)
assertThat(macValidatingStream.validationAttempted).isTrue()
macValidatingStream.close()
}
@Test
fun `failure - invalid MAC`() {
val data = "Hello, World!".toByteArray()
val key = Util.getSecretBytes(32)
val wrongKey = ByteArray(32) { 24 }
val dataWithMac = createDataWithMac(data, key)
val inputStream = ByteArrayInputStream(dataWithMac)
val mac = createMac(wrongKey) // Wrong key
val macValidatingStream = MacValidatingInputStream(inputStream, mac)
try {
macValidatingStream.readFully()
fail("Expected InvalidMessageException to be thrown")
} catch (e: InvalidMessageException) {
assertThat(e.message).isEqualTo("MAC validation failed!")
} finally {
macValidatingStream.close()
}
}
@Test
fun `failure - insufficient data for MAC`() {
val key = Util.getSecretBytes(32)
val mac = createMac(key)
val macLength = mac.macLength
val insufficientData = ByteArray(macLength - 1) { 5 } // Less than MAC length
val inputStream = ByteArrayInputStream(insufficientData)
val mac2 = createMac(key)
val macValidatingStream = MacValidatingInputStream(inputStream, mac2)
try {
macValidatingStream.readFully()
fail("Expected InvalidMessageException to be thrown")
} catch (e: InvalidMessageException) {
assertThat(e.message).isEqualTo("Stream ended before MAC could be read. Expected $macLength bytes, got ${insufficientData.size}")
} finally {
macValidatingStream.close()
}
}
private fun createMac(key: ByteArray): Mac {
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(key, "HmacSHA256"))
return mac
}
private fun createDataWithMac(data: ByteArray, key: ByteArray): ByteArray {
val mac = createMac(key)
val macBytes = mac.doFinal(data)
return data + macBytes
}
}

View File

@@ -0,0 +1,148 @@
package org.whispersystems.signalservice.api.crypto;
import org.conscrypt.Conscrypt;
import org.junit.Test;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.internal.util.Util;
import org.signal.core.util.Base64;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.Security;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.whispersystems.signalservice.testutil.LibSignalLibraryUtil.assumeLibSignalSupportedOnOS;
public class ProfileCipherTest {
private class TestByteArrayInputStream extends ByteArrayInputStream {
TestByteArrayInputStream(byte[] buffer) {
super(buffer);
}
int getPos() {
return this.pos;
}
}
static {
// https://github.com/google/conscrypt/issues/1034
if (!System.getProperty("os.arch").equals("aarch64")) {
Security.insertProviderAt(Conscrypt.newProvider(), 1);
}
}
@Test
public void testEncryptDecrypt() throws InvalidCiphertextException, InvalidInputException {
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ProfileCipher cipher = new ProfileCipher(key);
byte[] name = cipher.encrypt("Clement\0Duval".getBytes(), 53);
String plaintext = cipher.decryptString(name);
assertEquals(plaintext, "Clement\0Duval");
}
@Test
public void testEmpty() throws Exception {
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ProfileCipher cipher = new ProfileCipher(key);
byte[] name = cipher.encrypt("".getBytes(), 26);
String plaintext = cipher.decryptString(name);
assertEquals(plaintext.length(), 0);
}
private byte[] readStream(byte[] input, ProfileKey key, int bufferSize) throws Exception {
TestByteArrayInputStream bais = new TestByteArrayInputStream(input);
assertEquals(0, bais.getPos());
ProfileCipherInputStream in = new ProfileCipherInputStream(bais, key);
assertEquals(12 + 16, bais.getPos()); // initial read of nonce + tag-sized buffer
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[bufferSize];
int pos = bais.getPos();
int read;
while ((read = in.read(buffer)) != -1) {
assertEquals(pos + read, bais.getPos());
pos += read;
result.write(buffer, 0, read);
}
assertEquals(pos, input.length);
return result.toByteArray();
}
@Test
public void testStreams() throws Exception {
assumeLibSignalSupportedOnOS();
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ProfileCipherOutputStream out = new ProfileCipherOutputStream(baos, key);
out.write("This is an avatar".getBytes());
out.flush();
out.close();
byte[] encrypted = baos.toByteArray();
assertEquals(new String(readStream(encrypted, key, 2048)), "This is an avatar");
assertEquals(new String(readStream(encrypted, key, 16 /* == block size */)), "This is an avatar");
assertEquals(new String(readStream(encrypted, key, 5)), "This is an avatar");
}
@Test
public void testStreamBadAuthentication() throws Exception {
assumeLibSignalSupportedOnOS();
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ProfileCipherOutputStream out = new ProfileCipherOutputStream(baos, key);
out.write("This is an avatar".getBytes());
out.flush();
out.close();
byte[] encrypted = baos.toByteArray();
encrypted[encrypted.length - 1] ^= 1;
try {
readStream(encrypted, key, 2048);
fail("failed to verify authenticate tag");
} catch (IOException e) {
}
}
@Test
public void testEncryptLengthBucket1() throws InvalidInputException {
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ProfileCipher cipher = new ProfileCipher(key);
byte[] name = cipher.encrypt("Peter\0Parker".getBytes(), 53);
String encoded = Base64.encodeWithPadding(name);
assertEquals(108, encoded.length());
}
@Test
public void testEncryptLengthBucket2() throws InvalidInputException {
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ProfileCipher cipher = new ProfileCipher(key);
byte[] name = cipher.encrypt("Peter\0Parker".getBytes(), 257);
String encoded = Base64.encodeWithPadding(name);
assertEquals(380, encoded.length());
}
@Test
public void testTargetNameLength() {
assertEquals(53, ProfileCipher.getTargetNameLength("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"));
assertEquals(53, ProfileCipher.getTargetNameLength("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1"));
assertEquals(257, ProfileCipher.getTargetNameLength("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12"));
}
}

View File

@@ -0,0 +1,137 @@
package org.whispersystems.signalservice.api.crypto;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class SkippingOutputStreamTest {
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
@Test
public void givenZeroToSkip_whenIWriteInt_thenIGetIntInOutput() throws Exception {
// GIVEN
SkippingOutputStream testSubject = new SkippingOutputStream(0, outputStream);
// WHEN
testSubject.write(0);
// THEN
assertEquals(1, outputStream.toByteArray().length);
assertEquals(0, outputStream.toByteArray()[0]);
}
@Test
public void givenOneToSkip_whenIWriteIntTwice_thenIGetSecondIntInOutput() throws Exception {
// GIVEN
SkippingOutputStream testSubject = new SkippingOutputStream(1, outputStream);
// WHEN
testSubject.write(0);
testSubject.write(1);
// THEN
assertEquals(1, outputStream.toByteArray().length);
assertEquals(1, outputStream.toByteArray()[0]);
}
@Test
public void givenZeroToSkip_whenIWriteArray_thenIGetArrayInOutput() throws Exception {
// GIVEN
byte[] expected = new byte[]{1, 2, 3, 4, 5};
SkippingOutputStream testSubject = new SkippingOutputStream(0, outputStream);
// WHEN
testSubject.write(expected);
// THEN
assertEquals(expected.length, outputStream.toByteArray().length);
assertArrayEquals(expected, outputStream.toByteArray());
}
@Test
public void givenNonZeroToSkip_whenIWriteArray_thenIGetEndOfArrayInOutput() throws Exception {
// GIVEN
byte[] expected = new byte[]{1, 2, 3, 4, 5};
SkippingOutputStream testSubject = new SkippingOutputStream(3, outputStream);
// WHEN
testSubject.write(expected);
// THEN
assertEquals(2, outputStream.toByteArray().length);
assertArrayEquals(new byte[]{4, 5}, outputStream.toByteArray());
}
@Test
public void givenSkipGreaterThanByteArray_whenIWriteArray_thenIGetNoOutput() throws Exception {
// GIVEN
byte[] array = new byte[]{1, 2, 3, 4, 5};
SkippingOutputStream testSubject = new SkippingOutputStream(10, outputStream);
// WHEN
testSubject.write(array);
// THEN
assertEquals(0, outputStream.toByteArray().length);
}
@Test
public void givenZeroToSkip_whenIWriteArrayRange_thenIGetArrayRangeInOutput() throws Exception {
// GIVEN
byte[] expected = new byte[]{1, 2, 3, 4, 5};
SkippingOutputStream testSubject = new SkippingOutputStream(0, outputStream);
// WHEN
testSubject.write(expected, 1, 3);
// THEN
assertEquals(3, outputStream.toByteArray().length);
assertArrayEquals(new byte[]{2, 3, 4}, outputStream.toByteArray());
}
@Test
public void givenNonZeroToSkip_whenIWriteArrayRange_thenIGetEndOfArrayRangeInOutput() throws Exception {
// GIVEN
byte[] expected = new byte[]{1, 2, 3, 4, 5};
SkippingOutputStream testSubject = new SkippingOutputStream(1, outputStream);
// WHEN
testSubject.write(expected, 3, 2);
// THEN
assertEquals(1, outputStream.toByteArray().length);
assertArrayEquals(new byte[]{5}, outputStream.toByteArray());
}
@Test
public void givenSkipGreaterThanByteArrayRange_whenIWriteArrayRange_thenIGetNoOutput() throws Exception {
// GIVEN
byte[] array = new byte[]{1, 2, 3, 4, 5};
SkippingOutputStream testSubject = new SkippingOutputStream(10, outputStream);
// WHEN
testSubject.write(array, 3, 2);
// THEN
assertEquals(0, outputStream.toByteArray().length);
}
@Test
public void givenSkipGreaterThanByteArrayRange_whenIWriteArrayRangeTwice_thenIGetExpectedOutput() throws Exception {
// GIVEN
byte[] array = new byte[]{1, 2, 3, 4, 5};
SkippingOutputStream testSubject = new SkippingOutputStream(3, outputStream);
// WHEN
testSubject.write(array, 3, 2);
testSubject.write(array, 3, 2);
// THEN
assertEquals(1, outputStream.toByteArray().length);
assertArrayEquals(new byte[]{5}, outputStream.toByteArray());
}
}

View File

@@ -0,0 +1,32 @@
package org.whispersystems.signalservice.api.crypto;
import org.conscrypt.OpenSSLProvider;
import org.junit.Test;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import java.security.Security;
import java.util.Arrays;
import static org.junit.Assert.assertArrayEquals;
public class UnidentifiedAccessTest {
static {
// https://github.com/google/conscrypt/issues/1034
if (!System.getProperty("os.arch").equals("aarch64")) {
Security.insertProviderAt(new OpenSSLProvider(), 1);
}
}
private final byte[] EXPECTED_RESULT = {(byte)0x5a, (byte)0x72, (byte)0x3a, (byte)0xce, (byte)0xe5, (byte)0x2c, (byte)0x5e, (byte)0xa0, (byte)0x2b, (byte)0x92, (byte)0xa3, (byte)0xa3, (byte)0x60, (byte)0xc0, (byte)0x95, (byte)0x95};
@Test
public void testKeyDerivation() throws InvalidInputException {
byte[] key = new byte[32];
Arrays.fill(key, (byte)0x02);
byte[] result = UnidentifiedAccess.deriveAccessKeyFrom(new ProfileKey(key));
assertArrayEquals(EXPECTED_RESULT, result);
}
}

View File

@@ -0,0 +1,113 @@
package org.whispersystems.signalservice.api.groupsv2
import assertk.assertThat
import assertk.assertions.containsExactly
import assertk.assertions.isEqualTo
import assertk.assertions.isNull
import okio.ByteString
import org.junit.Test
import org.signal.core.models.ServiceId
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval
import org.whispersystems.signalservice.internal.util.Util
import java.util.UUID
class DecryptedGroupUtilTest {
@Test
fun can_extract_editor_uuid_from_decrypted_group_change() {
val aci = randomACI()
val editor = aci.toByteString()
val groupChange = DecryptedGroupChange.Builder()
.editorServiceIdBytes(editor)
.build()
val parsed = DecryptedGroupUtil.editorServiceId(groupChange).get()
assertThat(parsed).isEqualTo(aci)
}
@Test
fun can_extract_uuid_from_decrypted_pending_member() {
val aci = randomACI()
val decryptedMember = DecryptedPendingMember.Builder()
.serviceIdBytes(aci.toByteString())
.build()
val parsed = ServiceId.parseOrNull(decryptedMember.serviceIdBytes)
assertThat(parsed).isEqualTo(aci)
}
@Test
fun can_extract_uuid_from_bad_decrypted_pending_member() {
val decryptedMember = DecryptedPendingMember.Builder()
.serviceIdBytes(ByteString.of(*Util.getSecretBytes(18)))
.build()
val parsed = ServiceId.parseOrNull(decryptedMember.serviceIdBytes)
assertThat(parsed).isNull()
}
@Test
fun can_extract_uuids_for_all_pending_including_bad_entries() {
val aci1 = randomACI()
val aci2 = randomACI()
val decryptedMember1 = DecryptedPendingMember.Builder()
.serviceIdBytes(aci1.toByteString())
.build()
val decryptedMember2 = DecryptedPendingMember.Builder()
.serviceIdBytes(aci2.toByteString())
.build()
val decryptedMember3 = DecryptedPendingMember.Builder()
.serviceIdBytes(ByteString.of(*Util.getSecretBytes(18)))
.build()
val groupChange = DecryptedGroupChange.Builder()
.newPendingMembers(listOf(decryptedMember1, decryptedMember2, decryptedMember3))
.build()
val pendingUuids = DecryptedGroupUtil.pendingToServiceIdList(groupChange.newPendingMembers)
assertThat(pendingUuids).containsExactly(aci1, aci2, ServiceId.ACI.UNKNOWN)
}
@Test
fun can_extract_uuids_for_all_deleted_pending_excluding_bad_entries() {
val aci1 = randomACI()
val aci2 = randomACI()
val decryptedMember1 = DecryptedPendingMemberRemoval.Builder()
.serviceIdBytes(aci1.toByteString())
.build()
val decryptedMember2 = DecryptedPendingMemberRemoval.Builder()
.serviceIdBytes(aci2.toByteString())
.build()
val decryptedMember3 = DecryptedPendingMemberRemoval.Builder()
.serviceIdBytes(ByteString.of(*Util.getSecretBytes(18)))
.build()
val groupChange = DecryptedGroupChange.Builder()
.deletePendingMembers(listOf(decryptedMember1, decryptedMember2, decryptedMember3))
.build()
val removedUuids = DecryptedGroupUtil.removedPendingMembersServiceIdList(groupChange)
assertThat(removedUuids).containsExactly(aci1, aci2)
}
@Test
fun can_extract_uuids_for_all_deleted_members_excluding_bad_entries() {
val aci1 = randomACI()
val aci2 = randomACI()
val groupChange = DecryptedGroupChange.Builder()
.deleteMembers(listOf(aci1.toByteString(), aci2.toByteString(), ByteString.of(*Util.getSecretBytes(18))))
.build()
val removedServiceIds = DecryptedGroupUtil.removedMembersServiceIdList(groupChange)
assertThat(removedServiceIds).containsExactly(aci1, aci2)
}
private fun randomACI() = ServiceId.ACI.from(UUID.randomUUID())
}

View File

@@ -0,0 +1,959 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.junit.Test;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.core.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.util.List;
import java.util.UUID;
import okio.ByteString;
import static org.junit.Assert.assertEquals;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.asAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.asMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.bannedMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.newProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingPniAciMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.requestingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.withProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class DecryptedGroupUtil_apply_Test {
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be applied by {@link DecryptedGroupUtil#apply}.
*/
@Test
public void ensure_DecryptedGroupUtil_knows_about_all_fields_of_DecryptedGroupChange() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("DecryptedGroupUtil and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
24, maxFieldFound);
}
@Test
public void apply_revision() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(9)
.build(),
new DecryptedGroupChange.Builder()
.revision(10)
.build());
assertEquals(10, newGroup.revision);
}
@Test
public void apply_new_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedMember member2 = member(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newMembers(List.of(member2))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1, member2))
.build(),
newGroup);
}
@Test
public void apply_new_member_already_in_the_group() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedMember member2 = member(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1, member2))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newMembers(List.of(member2))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1, member2))
.build(),
newGroup);
}
@Test
public void apply_new_member_already_in_the_group_by_uuid() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
UUID member2Uuid = UUID.randomUUID();
DecryptedMember member2a = member(member2Uuid, newProfileKey());
DecryptedMember member2b = member(member2Uuid, newProfileKey());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1, member2a))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newMembers(List.of(member2b))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1, member2b))
.build(),
newGroup);
}
@Test
public void apply_remove_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedMember member2 = member(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(13)
.members(List.of(member1, member2))
.build(),
new DecryptedGroupChange.Builder()
.revision(14)
.deleteMembers(List.of(member1.aciBytes))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(14)
.members(List.of(member2))
.build(),
newGroup);
}
@Test
public void apply_remove_members() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedMember member2 = member(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(13)
.members(List.of(member1, member2))
.build(),
new DecryptedGroupChange.Builder()
.revision(14)
.deleteMembers(List.of(member1.aciBytes, member2.aciBytes))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(14)
.build(),
newGroup);
}
@Test
public void apply_remove_members_not_found() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedMember member2 = member(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(13)
.members(List.of(member1))
.build(),
new DecryptedGroupChange.Builder()
.revision(14)
.deleteMembers(List.of(member2.aciBytes))
.build());
assertEquals(new DecryptedGroup.Builder()
.members(List.of(member1))
.revision(14)
.build(),
newGroup);
}
@Test
public void apply_modify_member_role() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedMember member2 = admin(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(13)
.members(List.of(member1, member2))
.build(),
new DecryptedGroupChange.Builder()
.revision(14)
.modifyMemberRoles(List.of(new DecryptedModifyMemberRole.Builder().aciBytes(member1.aciBytes).role(Member.Role.ADMINISTRATOR).build(),
new DecryptedModifyMemberRole.Builder().aciBytes(member2.aciBytes).role(Member.Role.DEFAULT).build()))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(14)
.members(List.of(asAdmin(member1), asMember(member2)))
.build(),
newGroup);
}
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
public void not_able_to_apply_modify_member_role_for_non_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedMember member2 = member(UUID.randomUUID());
DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(13)
.members(List.of(member1))
.build(),
new DecryptedGroupChange.Builder()
.revision(14)
.modifyMemberRoles(List.of(new DecryptedModifyMemberRole.Builder()
.role(Member.Role.ADMINISTRATOR)
.aciBytes(member2.aciBytes)
.build()))
.build());
}
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
public void not_able_to_apply_modify_member_role_for_no_role() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(13)
.members(List.of(member1))
.build(),
new DecryptedGroupChange.Builder()
.revision(14)
.modifyMemberRoles(List.of(new DecryptedModifyMemberRole.Builder()
.aciBytes(member1.aciBytes)
.build()))
.build());
}
@Test
public void apply_modify_member_profile_keys() throws NotAbleToApplyGroupV2ChangeException {
ProfileKey profileKey1 = randomProfileKey();
ProfileKey profileKey2a = randomProfileKey();
ProfileKey profileKey2b = randomProfileKey();
DecryptedMember member1 = member(UUID.randomUUID(), profileKey1);
DecryptedMember member2a = member(UUID.randomUUID(), profileKey2a);
DecryptedMember member2b = withProfileKey(member2a, profileKey2b);
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(13)
.members(List.of(member1, member2a))
.build(),
new DecryptedGroupChange.Builder()
.revision(14)
.modifiedProfileKeys(List.of(member2b))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(14)
.members(List.of(member1, member2b))
.build(),
newGroup);
}
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
public void cant_apply_modify_member_profile_keys_if_member_not_in_group() throws NotAbleToApplyGroupV2ChangeException {
ProfileKey profileKey1 = randomProfileKey();
ProfileKey profileKey2a = randomProfileKey();
ProfileKey profileKey2b = randomProfileKey();
DecryptedMember member1 = member(UUID.randomUUID(), profileKey1);
DecryptedMember member2a = member(UUID.randomUUID(), profileKey2a);
DecryptedMember member2b = member(UUID.randomUUID(), profileKey2b);
DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(13)
.members(List.of(member1, member2a))
.build(),
new DecryptedGroupChange.Builder()
.revision(14)
.modifiedProfileKeys(List.of(member2b))
.build());
}
@Test
public void apply_modify_admin_profile_keys() throws NotAbleToApplyGroupV2ChangeException {
UUID adminUuid = UUID.randomUUID();
ProfileKey profileKey1 = randomProfileKey();
ProfileKey profileKey2a = randomProfileKey();
ProfileKey profileKey2b = randomProfileKey();
DecryptedMember member1 = member(UUID.randomUUID(), profileKey1);
DecryptedMember admin2a = admin(adminUuid, profileKey2a);
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(13)
.members(List.of(member1, admin2a))
.build(),
new DecryptedGroupChange.Builder()
.revision(14)
.modifiedProfileKeys(List.of(new DecryptedMember.Builder()
.aciBytes(UuidUtil.toByteString(adminUuid))
.build()
.newBuilder()
.profileKey(ByteString.of(profileKey2b.serialize()))
.build()))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(14)
.members(List.of(member1, admin(adminUuid, profileKey2b)))
.build(),
newGroup);
}
@Test
public void apply_new_pending_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedPendingMember pending = pendingMember(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newPendingMembers(List.of(pending))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1))
.pendingMembers(List.of(pending))
.build(),
newGroup);
}
@Test
public void apply_new_pending_member_already_pending() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedPendingMember pending = pendingMember(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1))
.pendingMembers(List.of(pending))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newPendingMembers(List.of(pending))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1))
.pendingMembers(List.of(pending))
.build(),
newGroup);
}
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
public void apply_new_pending_member_already_in_group() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
UUID uuid2 = UUID.randomUUID();
DecryptedMember member2 = member(uuid2);
DecryptedPendingMember pending2 = pendingMember(uuid2);
DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1, member2))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newPendingMembers(List.of(pending2))
.build());
}
@Test
public void remove_pending_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
UUID pendingUuid = UUID.randomUUID();
DecryptedPendingMember pending = pendingMember(pendingUuid);
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1))
.pendingMembers(List.of(pending))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.deletePendingMembers(List.of(new DecryptedPendingMemberRemoval.Builder()
.serviceIdCipherText(ProtoTestUtils.encrypt(pendingUuid))
.build()))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1))
.build(),
newGroup);
}
@Test
public void cannot_remove_pending_member_if_not_in_group() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
UUID pendingUuid = UUID.randomUUID();
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.deletePendingMembers(List.of(new DecryptedPendingMemberRemoval.Builder()
.serviceIdCipherText(ProtoTestUtils.encrypt(pendingUuid))
.build()))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1))
.build(),
newGroup);
}
@Test
public void promote_pending_member() throws NotAbleToApplyGroupV2ChangeException {
ProfileKey profileKey2 = randomProfileKey();
DecryptedMember member1 = member(UUID.randomUUID());
UUID pending2Uuid = UUID.randomUUID();
DecryptedPendingMember pending2 = pendingMember(pending2Uuid);
DecryptedMember member2 = member(pending2Uuid, profileKey2);
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1))
.pendingMembers(List.of(pending2))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.promotePendingMembers(List.of(member2))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1, member2))
.build(),
newGroup);
}
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
public void cannot_promote_pending_member_if_not_in_group() throws NotAbleToApplyGroupV2ChangeException {
ProfileKey profileKey2 = randomProfileKey();
DecryptedMember member1 = member(UUID.randomUUID());
UUID pending2Uuid = UUID.randomUUID();
DecryptedMember member2 = withProfileKey(admin(pending2Uuid), profileKey2);
DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.promotePendingMembers(List.of(member2))
.build());
}
@Test
public void skip_promote_pending_member_by_direct_add() throws NotAbleToApplyGroupV2ChangeException {
ProfileKey profileKey2 = randomProfileKey();
ProfileKey profileKey3 = randomProfileKey();
DecryptedMember member1 = member(UUID.randomUUID());
UUID pending2Uuid = UUID.randomUUID();
UUID pending3Uuid = UUID.randomUUID();
UUID pending4Uuid = UUID.randomUUID();
DecryptedPendingMember pending2 = pendingMember(pending2Uuid);
DecryptedPendingMember pending3 = pendingMember(pending3Uuid);
DecryptedPendingMember pending4 = pendingMember(pending4Uuid);
DecryptedMember member2 = member(pending2Uuid, profileKey2);
DecryptedMember member3 = member(pending3Uuid, profileKey3);
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1))
.pendingMembers(List.of(pending2, pending3, pending4))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newMembers(List.of(member2, member3))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1, member2, member3))
.pendingMembers(List.of(pending4))
.build(),
newGroup);
}
@Test
public void skip_promote_requesting_member_by_direct_add() throws NotAbleToApplyGroupV2ChangeException {
ProfileKey profileKey2 = randomProfileKey();
ProfileKey profileKey3 = randomProfileKey();
DecryptedMember member1 = member(UUID.randomUUID());
UUID requesting2Uuid = UUID.randomUUID();
UUID requesting3Uuid = UUID.randomUUID();
UUID requesting4Uuid = UUID.randomUUID();
DecryptedRequestingMember requesting2 = requestingMember(requesting2Uuid);
DecryptedRequestingMember requesting3 = requestingMember(requesting3Uuid);
DecryptedRequestingMember requesting4 = requestingMember(requesting4Uuid);
DecryptedMember member2 = member(requesting2Uuid, profileKey2);
DecryptedMember member3 = member(requesting3Uuid, profileKey3);
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1))
.requestingMembers(List.of(requesting2, requesting3, requesting4))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newMembers(List.of(member2, member3))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1, member2, member3))
.requestingMembers(List.of(requesting4))
.build(),
newGroup);
}
@Test
public void title() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.title("Old title")
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newTitle(new DecryptedString.Builder().value_("New title").build())
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.title("New title")
.build(),
newGroup);
}
@Test
public void description() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.description("Old description")
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newDescription(new DecryptedString.Builder().value_("New Description").build())
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.description("New Description")
.build(),
newGroup);
}
@Test
public void isAnnouncementGroup() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.isAnnouncementGroup(EnabledState.DISABLED)
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newIsAnnouncementGroup(EnabledState.ENABLED)
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.isAnnouncementGroup(EnabledState.ENABLED)
.build(),
newGroup);
}
@Test
public void avatar() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.avatar("https://cnd/oldavatar")
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newAvatar(new DecryptedString.Builder().value_("https://cnd/newavatar").build())
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.avatar("https://cnd/newavatar")
.build(),
newGroup);
}
@Test
public void timer() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.disappearingMessagesTimer(new DecryptedTimer.Builder().duration(100).build())
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newTimer(new DecryptedTimer.Builder().duration(2000).build())
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.disappearingMessagesTimer(new DecryptedTimer.Builder().duration(2000).build())
.build(),
newGroup);
}
@Test
public void attribute_access() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.accessControl(new AccessControl.Builder()
.attributes(AccessControl.AccessRequired.ADMINISTRATOR)
.members(AccessControl.AccessRequired.MEMBER)
.build())
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newAttributeAccess(AccessControl.AccessRequired.MEMBER)
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.accessControl(new AccessControl.Builder()
.attributes(AccessControl.AccessRequired.MEMBER)
.members(AccessControl.AccessRequired.MEMBER)
.build())
.build(),
newGroup);
}
@Test
public void membership_access() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.accessControl(new AccessControl.Builder()
.attributes(AccessControl.AccessRequired.ADMINISTRATOR)
.members(AccessControl.AccessRequired.MEMBER)
.build())
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.accessControl(new AccessControl.Builder()
.attributes(AccessControl.AccessRequired.ADMINISTRATOR)
.members(AccessControl.AccessRequired.ADMINISTRATOR)
.build())
.build(),
newGroup);
}
@Test
public void change_both_access_levels() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.accessControl(new AccessControl.Builder()
.attributes(AccessControl.AccessRequired.ADMINISTRATOR)
.members(AccessControl.AccessRequired.MEMBER)
.build())
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newAttributeAccess(AccessControl.AccessRequired.MEMBER)
.newMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.accessControl(new AccessControl.Builder()
.attributes(AccessControl.AccessRequired.MEMBER)
.members(AccessControl.AccessRequired.ADMINISTRATOR)
.build())
.build(),
newGroup);
}
@Test
public void invite_link_access() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.accessControl(new AccessControl.Builder()
.attributes(AccessControl.AccessRequired.MEMBER)
.members(AccessControl.AccessRequired.MEMBER)
.addFromInviteLink(AccessControl.AccessRequired.UNSATISFIABLE)
.build())
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.accessControl(new AccessControl.Builder()
.attributes(AccessControl.AccessRequired.MEMBER)
.members(AccessControl.AccessRequired.MEMBER)
.addFromInviteLink(AccessControl.AccessRequired.ADMINISTRATOR)
.build())
.build(),
newGroup);
}
@Test
public void apply_new_requesting_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedRequestingMember member1 = requestingMember(UUID.randomUUID());
DecryptedRequestingMember member2 = requestingMember(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.requestingMembers(List.of(member1))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newRequestingMembers(List.of(member2))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.requestingMembers(List.of(member1, member2))
.build(),
newGroup);
}
@Test
public void apply_remove_requesting_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedRequestingMember member1 = requestingMember(UUID.randomUUID());
DecryptedRequestingMember member2 = requestingMember(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(13)
.requestingMembers(List.of(member1, member2))
.build(),
new DecryptedGroupChange.Builder()
.revision(14)
.deleteRequestingMembers(List.of(member1.aciBytes))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(14)
.requestingMembers(List.of(member2))
.build(),
newGroup);
}
@Test
public void promote_requesting_member() throws NotAbleToApplyGroupV2ChangeException {
UUID uuid1 = UUID.randomUUID();
UUID uuid2 = UUID.randomUUID();
UUID uuid3 = UUID.randomUUID();
ProfileKey profileKey1 = newProfileKey();
ProfileKey profileKey2 = newProfileKey();
ProfileKey profileKey3 = newProfileKey();
DecryptedRequestingMember member1 = requestingMember(uuid1, profileKey1);
DecryptedRequestingMember member2 = requestingMember(uuid2, profileKey2);
DecryptedRequestingMember member3 = requestingMember(uuid3, profileKey3);
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(13)
.requestingMembers(List.of(member1, member2, member3))
.build(),
new DecryptedGroupChange.Builder()
.revision(14)
.promoteRequestingMembers(List.of(new DecryptedApproveMember.Builder()
.role(Member.Role.DEFAULT)
.aciBytes(member1.aciBytes)
.build(),
new DecryptedApproveMember.Builder()
.role(Member.Role.ADMINISTRATOR)
.aciBytes(member2.aciBytes)
.build()))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(14)
.members(List.of(member(uuid1, profileKey1), admin(uuid2, profileKey2)))
.requestingMembers(List.of(member3))
.build(),
newGroup);
}
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
public void cannot_apply_promote_requesting_member_without_a_role() throws NotAbleToApplyGroupV2ChangeException {
UUID uuid = UUID.randomUUID();
DecryptedRequestingMember member = requestingMember(uuid);
DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(13)
.requestingMembers(List.of(member))
.build(),
new DecryptedGroupChange.Builder()
.revision(14)
.promoteRequestingMembers(List.of(new DecryptedApproveMember.Builder().aciBytes(member.aciBytes).build()))
.build());
}
@Test
public void invite_link_password() throws NotAbleToApplyGroupV2ChangeException {
ByteString password1 = ByteString.of(Util.getSecretBytes(16));
ByteString password2 = ByteString.of(Util.getSecretBytes(16));
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.inviteLinkPassword(password1)
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newInviteLinkPassword(password2)
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.inviteLinkPassword(password2)
.build(),
newGroup);
}
@Test
public void invite_link_password_not_changed() throws NotAbleToApplyGroupV2ChangeException {
ByteString password = ByteString.of(Util.getSecretBytes(16));
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.inviteLinkPassword(password)
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.inviteLinkPassword(password)
.build(),
newGroup);
}
@Test
public void apply_new_banned_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedBannedMember banned = bannedMember(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newBannedMembers(List.of(banned))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1))
.bannedMembers(List.of(banned))
.build(),
newGroup);
}
@Test
public void apply_new_banned_member_already_banned() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedBannedMember banned = bannedMember(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1))
.bannedMembers(List.of(banned))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.newBannedMembers(List.of(banned))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1))
.bannedMembers(List.of(banned))
.build(),
newGroup);
}
@Test
public void remove_banned_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
UUID bannedUuid = UUID.randomUUID();
DecryptedBannedMember banned = bannedMember(bannedUuid);
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1))
.bannedMembers(List.of(banned))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.deleteBannedMembers(List.of(new DecryptedBannedMember.Builder()
.serviceIdBytes(UuidUtil.toByteString(bannedUuid))
.build()))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1))
.build(),
newGroup);
}
@Test
public void promote_pending_member_pni_aci() throws NotAbleToApplyGroupV2ChangeException {
ProfileKey profileKey2 = randomProfileKey();
DecryptedMember member1 = member(UUID.randomUUID());
UUID pending2Aci = UUID.randomUUID();
UUID pending2Pni = UUID.randomUUID();
DecryptedPendingMember pending2 = pendingMember(pending2Pni);
DecryptedMember member2 = pendingPniAciMember(pending2Aci, pending2Pni, profileKey2);
DecryptedGroup newGroup = DecryptedGroupUtil.apply(new DecryptedGroup.Builder()
.revision(10)
.members(List.of(member1))
.pendingMembers(List.of(pending2))
.build(),
new DecryptedGroupChange.Builder()
.revision(11)
.promotePendingPniAciMembers(List.of(member2))
.build());
assertEquals(new DecryptedGroup.Builder()
.revision(11)
.members(List.of(member1, member2))
.build(),
newGroup);
}
}

View File

@@ -0,0 +1,269 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.junit.Test;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.core.util.UuidUtil;
import java.util.List;
import java.util.UUID;
import okio.ByteString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMemberRemoval;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingPniAciMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.promoteAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class DecryptedGroupUtil_empty_Test {
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would easily affect {@link DecryptedGroupUtil}'s ability to detect non-empty change states.
*/
@Test
public void ensure_DecryptedGroupUtil_knows_about_all_fields_of_DecryptedGroupChange() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("DecryptedGroupUtil and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
24, maxFieldFound);
}
@Test
public void empty_change_set() {
assertTrue(DecryptedGroupUtil.changeIsEmpty(new DecryptedGroupChange.Builder().build()));
}
@Test
public void not_empty_with_add_member_field_3() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newMembers(List.of(member(UUID.randomUUID())))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_delete_member_field_4() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.deleteMembers(List.of(UuidUtil.toByteString(UUID.randomUUID())))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_modify_member_roles_field_5() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.modifyMemberRoles(List.of(promoteAdmin(UUID.randomUUID())))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_modify_profile_keys_field_6() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.modifiedProfileKeys(List.of(member(UUID.randomUUID(), randomProfileKey())))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertTrue(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_add_pending_members_field_7() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newPendingMembers(List.of(pendingMember(UUID.randomUUID())))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_delete_pending_members_field_8() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.deletePendingMembers(List.of(pendingMemberRemoval(UUID.randomUUID())))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_promote_delete_pending_members_field_9() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.promotePendingMembers(List.of(member(UUID.randomUUID())))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_modify_title_field_10() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newTitle(new DecryptedString.Builder().value_("New title").build())
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_modify_avatar_field_11() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newAvatar(new DecryptedString.Builder().value_("New Avatar").build())
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_modify_disappearing_message_timer_field_12() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newTimer(new DecryptedTimer.Builder().duration(60).build())
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_modify_attributes_field_13() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newAttributeAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_modify_member_access_field_14() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newMemberAccess(AccessControl.AccessRequired.MEMBER)
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_modify_add_from_invite_link_access_field_15() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_an_add_requesting_member_field_16() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newRequestingMembers(List.of(new DecryptedRequestingMember()))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_a_delete_requesting_member_field_17() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.deleteRequestingMembers(List.of(ByteString.of(new byte[16])))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_a_promote_requesting_member_field_18() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.promoteRequestingMembers(List.of(new DecryptedApproveMember()))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_a_new_invite_link_password_19() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newInviteLinkPassword(ByteString.of(new byte[16]))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_modify_description_field_20() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newDescription(new DecryptedString.Builder().value_("New description").build())
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_modify_announcement_field_21() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newIsAnnouncementGroup(EnabledState.ENABLED)
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_add_banned_member_field_22() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.newBannedMembers(List.of(new DecryptedBannedMember()))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_delete_banned_member_field_23() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.deleteBannedMembers(List.of(new DecryptedBannedMember()))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_promote_pending_pni_aci_members_field_24() {
DecryptedGroupChange change = new DecryptedGroupChange.Builder()
.promotePendingPniAciMembers(List.of(pendingPniAciMember(UUID.randomUUID(), UUID.randomUUID(), randomProfileKey())))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
}

View File

@@ -0,0 +1,411 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.junit.Test;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.core.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.util.List;
import java.util.UUID;
import okio.ByteString;
import static org.junit.Assert.assertEquals;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.bannedMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.demoteAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.newProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMemberRemoval;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.promoteAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.requestingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.withProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class GroupChangeReconstructTest {
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be detected by {@link GroupChangeReconstruct#reconstructGroupChange}.
*/
@Test
public void ensure_GroupChangeReconstruct_knows_about_all_fields_of_DecryptedGroup() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroup.class, ProtobufTestUtils.IGNORED_DECRYPTED_GROUP_TAGS);
assertEquals("GroupChangeReconstruct and its tests need updating to account for new fields on " + DecryptedGroup.class.getName(),
13, maxFieldFound);
}
@Test
public void empty_to_empty() {
DecryptedGroup from = new DecryptedGroup.Builder().build();
DecryptedGroup to = new DecryptedGroup.Builder().build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().build(), decryptedGroupChange);
}
@Test
public void revision_set_to_the_target() {
DecryptedGroup from = new DecryptedGroup.Builder().revision(10).build();
DecryptedGroup to = new DecryptedGroup.Builder().revision(20).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(20, decryptedGroupChange.revision);
}
@Test
public void title_change() {
DecryptedGroup from = new DecryptedGroup.Builder().title("A").build();
DecryptedGroup to = new DecryptedGroup.Builder().title("B").build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().newTitle(new DecryptedString.Builder().value_("B").build()).build(), decryptedGroupChange);
}
@Test
public void description_change() {
DecryptedGroup from = new DecryptedGroup.Builder().description("A").build();
DecryptedGroup to = new DecryptedGroup.Builder().description("B").build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().newDescription(new DecryptedString.Builder().value_("B").build()).build(), decryptedGroupChange);
}
@Test
public void announcement_group_change() {
DecryptedGroup from = new DecryptedGroup.Builder().isAnnouncementGroup(EnabledState.DISABLED).build();
DecryptedGroup to = new DecryptedGroup.Builder().isAnnouncementGroup(EnabledState.ENABLED).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().newIsAnnouncementGroup(EnabledState.ENABLED).build(), decryptedGroupChange);
}
@Test
public void avatar_change() {
DecryptedGroup from = new DecryptedGroup.Builder().avatar("A").build();
DecryptedGroup to = new DecryptedGroup.Builder().avatar("B").build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().newAvatar(new DecryptedString.Builder().value_("B").build()).build(), decryptedGroupChange);
}
@Test
public void timer_change() {
DecryptedGroup from = new DecryptedGroup.Builder().disappearingMessagesTimer(new DecryptedTimer.Builder().duration(100).build()).build();
DecryptedGroup to = new DecryptedGroup.Builder().disappearingMessagesTimer(new DecryptedTimer.Builder().duration(200).build()).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().newTimer(new DecryptedTimer.Builder().duration(200).build()).build(), decryptedGroupChange);
}
@Test
public void access_control_change_attributes() {
DecryptedGroup from = new DecryptedGroup.Builder().accessControl(new AccessControl.Builder().attributes(AccessControl.AccessRequired.MEMBER).build()).build();
DecryptedGroup to = new DecryptedGroup.Builder().accessControl(new AccessControl.Builder().attributes(AccessControl.AccessRequired.ADMINISTRATOR).build()).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().newAttributeAccess(AccessControl.AccessRequired.ADMINISTRATOR).build(), decryptedGroupChange);
}
@Test
public void access_control_change_membership() {
DecryptedGroup from = new DecryptedGroup.Builder().accessControl(new AccessControl.Builder().members(AccessControl.AccessRequired.ADMINISTRATOR).build()).build();
DecryptedGroup to = new DecryptedGroup.Builder().accessControl(new AccessControl.Builder().members(AccessControl.AccessRequired.MEMBER).build()).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().newMemberAccess(AccessControl.AccessRequired.MEMBER).build(), decryptedGroupChange);
}
@Test
public void access_control_change_membership_and_attributes() {
DecryptedGroup from = new DecryptedGroup.Builder().accessControl(new AccessControl.Builder().members(AccessControl.AccessRequired.MEMBER)
.attributes(AccessControl.AccessRequired.ADMINISTRATOR).build()).build();
DecryptedGroup to = new DecryptedGroup.Builder().accessControl(new AccessControl.Builder().members(AccessControl.AccessRequired.ADMINISTRATOR)
.attributes(AccessControl.AccessRequired.MEMBER).build()).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().newMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.newAttributeAccess(AccessControl.AccessRequired.MEMBER).build(), decryptedGroupChange);
}
@Test
public void new_member() {
UUID uuidNew = UUID.randomUUID();
DecryptedGroup from = new DecryptedGroup.Builder().build();
DecryptedGroup to = new DecryptedGroup.Builder().members(List.of(member(uuidNew))).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().newMembers(List.of(member(uuidNew))).build(), decryptedGroupChange);
}
@Test
public void removed_member() {
UUID uuidOld = UUID.randomUUID();
DecryptedGroup from = new DecryptedGroup.Builder().members(List.of(member(uuidOld))).build();
DecryptedGroup to = new DecryptedGroup.Builder().build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().deleteMembers(List.of(UuidUtil.toByteString(uuidOld))).build(), decryptedGroupChange);
}
@Test
public void new_member_and_existing_member() {
UUID uuidOld = UUID.randomUUID();
UUID uuidNew = UUID.randomUUID();
DecryptedGroup from = new DecryptedGroup.Builder().members(List.of(member(uuidOld))).build();
DecryptedGroup to = new DecryptedGroup.Builder().members(List.of(member(uuidOld), member(uuidNew))).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().newMembers(List.of(member(uuidNew))).build(), decryptedGroupChange);
}
@Test
public void removed_member_and_remaining_member() {
UUID uuidOld = UUID.randomUUID();
UUID uuidRemaining = UUID.randomUUID();
DecryptedGroup from = new DecryptedGroup.Builder().members(List.of(member(uuidOld), member(uuidRemaining))).build();
DecryptedGroup to = new DecryptedGroup.Builder().members(List.of(member(uuidRemaining))).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().deleteMembers(List.of(UuidUtil.toByteString(uuidOld))).build(), decryptedGroupChange);
}
@Test
public void new_member_by_invite() {
UUID uuidNew = UUID.randomUUID();
DecryptedGroup from = new DecryptedGroup.Builder().pendingMembers(List.of(pendingMember(uuidNew))).build();
DecryptedGroup to = new DecryptedGroup.Builder().members(List.of(member(uuidNew))).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().promotePendingMembers(List.of(member(uuidNew))).build(), decryptedGroupChange);
}
@Test
public void uninvited_member_by_invite() {
UUID uuidNew = UUID.randomUUID();
DecryptedGroup from = new DecryptedGroup.Builder().pendingMembers(List.of(pendingMember(uuidNew))).build();
DecryptedGroup to = new DecryptedGroup.Builder().build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().deletePendingMembers(List.of(pendingMemberRemoval(uuidNew))).build(), decryptedGroupChange);
}
@Test
public void new_invite() {
UUID uuidNew = UUID.randomUUID();
DecryptedGroup from = new DecryptedGroup.Builder().build();
DecryptedGroup to = new DecryptedGroup.Builder().pendingMembers(List.of(pendingMember(uuidNew))).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().newPendingMembers(List.of(pendingMember(uuidNew))).build(), decryptedGroupChange);
}
@Test
public void to_admin() {
UUID uuid = UUID.randomUUID();
ProfileKey profileKey = randomProfileKey();
DecryptedGroup from = new DecryptedGroup.Builder().members(List.of(withProfileKey(member(uuid), profileKey))).build();
DecryptedGroup to = new DecryptedGroup.Builder().members(List.of(withProfileKey(admin(uuid), profileKey))).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().modifyMemberRoles(List.of(promoteAdmin(uuid))).build(), decryptedGroupChange);
}
@Test
public void to_member() {
UUID uuid = UUID.randomUUID();
ProfileKey profileKey = randomProfileKey();
DecryptedGroup from = new DecryptedGroup.Builder().members(List.of(withProfileKey(admin(uuid), profileKey))).build();
DecryptedGroup to = new DecryptedGroup.Builder().members(List.of(withProfileKey(member(uuid), profileKey))).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().modifyMemberRoles(List.of(demoteAdmin(uuid))).build(), decryptedGroupChange);
}
@Test
public void profile_key_change_member() {
UUID uuid = UUID.randomUUID();
ProfileKey profileKey1 = randomProfileKey();
ProfileKey profileKey2 = randomProfileKey();
DecryptedGroup from = new DecryptedGroup.Builder().members(List.of(withProfileKey(admin(uuid), profileKey1))).build();
DecryptedGroup to = new DecryptedGroup.Builder().members(List.of(withProfileKey(admin(uuid), profileKey2))).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().modifiedProfileKeys(List.of(withProfileKey(admin(uuid), profileKey2))).build(), decryptedGroupChange);
}
@Test
public void new_invite_access() {
DecryptedGroup from = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder()
.addFromInviteLink(AccessControl.AccessRequired.ADMINISTRATOR)
.build())
.build();
DecryptedGroup to = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder()
.addFromInviteLink(AccessControl.AccessRequired.UNSATISFIABLE)
.build())
.build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder()
.newInviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE)
.build(),
decryptedGroupChange);
}
@Test
public void new_requesting_members() {
UUID member1 = UUID.randomUUID();
ProfileKey profileKey1 = newProfileKey();
DecryptedGroup from = new DecryptedGroup.Builder()
.build();
DecryptedGroup to = new DecryptedGroup.Builder()
.requestingMembers(List.of(requestingMember(member1, profileKey1)))
.build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder()
.newRequestingMembers(List.of(requestingMember(member1, profileKey1)))
.build(),
decryptedGroupChange);
}
@Test
public void new_requesting_members_ignores_existing_by_uuid() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
ProfileKey profileKey2 = newProfileKey();
DecryptedGroup from = new DecryptedGroup.Builder()
.requestingMembers(List.of(requestingMember(member1, newProfileKey())))
.build();
DecryptedGroup to = new DecryptedGroup.Builder()
.requestingMembers(List.of(requestingMember(member1, newProfileKey()), requestingMember(member2, profileKey2)))
.build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder()
.newRequestingMembers(List.of(requestingMember(member2, profileKey2)))
.build(),
decryptedGroupChange);
}
@Test
public void removed_requesting_members() {
UUID member1 = UUID.randomUUID();
DecryptedGroup from = new DecryptedGroup.Builder()
.requestingMembers(List.of(requestingMember(member1, newProfileKey())))
.build();
DecryptedGroup to = new DecryptedGroup.Builder()
.build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder()
.deleteRequestingMembers(List.of(UuidUtil.toByteString(member1)))
.build(),
decryptedGroupChange);
}
@Test
public void promote_requesting_members() {
UUID member1 = UUID.randomUUID();
ProfileKey profileKey1 = newProfileKey();
UUID member2 = UUID.randomUUID();
ProfileKey profileKey2 = newProfileKey();
DecryptedGroup from = new DecryptedGroup.Builder()
.requestingMembers(List.of(requestingMember(member1, profileKey1)))
.requestingMembers(List.of(requestingMember(member2, profileKey2)))
.build();
DecryptedGroup to = new DecryptedGroup.Builder()
.members(List.of(member(member1, profileKey1)))
.members(List.of(admin(member2, profileKey2)))
.build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder()
.promoteRequestingMembers(List.of(approveMember(member1)))
.promoteRequestingMembers(List.of(approveAdmin(member2)))
.build(),
decryptedGroupChange);
}
@Test
public void new_invite_link_password() {
ByteString password1 = ByteString.of(Util.getSecretBytes(16));
ByteString password2 = ByteString.of(Util.getSecretBytes(16));
DecryptedGroup from = new DecryptedGroup.Builder()
.inviteLinkPassword(password1)
.build();
DecryptedGroup to = new DecryptedGroup.Builder()
.inviteLinkPassword(password2)
.build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder()
.newInviteLinkPassword(password2)
.build(),
decryptedGroupChange);
}
@Test
public void new_banned_member() {
UUID uuidNew = UUID.randomUUID();
DecryptedGroup from = new DecryptedGroup.Builder().build();
DecryptedGroup to = new DecryptedGroup.Builder().bannedMembers(List.of(bannedMember(uuidNew))).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().newBannedMembers(List.of(bannedMember(uuidNew))).build(), decryptedGroupChange);
}
@Test
public void removed_banned_member() {
UUID uuidOld = UUID.randomUUID();
DecryptedGroup from = new DecryptedGroup.Builder().bannedMembers(List.of(bannedMember(uuidOld))).build();
DecryptedGroup to = new DecryptedGroup.Builder().build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(new DecryptedGroupChange.Builder().deleteBannedMembers(List.of(bannedMember(uuidOld))).build(), decryptedGroupChange);
}
}

View File

@@ -0,0 +1,230 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.junit.Test;
import org.signal.storageservice.protos.groups.GroupChange;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class GroupChangeUtil_changeIsEmpty_Test {
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would easily affect {@link GroupChangeUtil}'s ability to detect empty change states and resolve conflicts.
*/
@Test
public void ensure_GroupChangeUtil_knows_about_all_fields_of_GroupChange_Actions() {
int maxFieldFound = getMaxDeclaredFieldNumber(GroupChange.Actions.class);
assertEquals("GroupChangeUtil and its tests need updating to account for new fields on " + GroupChange.Actions.class.getName(),
25, maxFieldFound);
}
@Test
public void empty_change_set() {
assertTrue(GroupChangeUtil.changeIsEmpty(new GroupChange.Actions.Builder().build()));
}
@Test
public void not_empty_with_add_member_field_3() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.addMembers(List.of(new GroupChange.Actions.AddMemberAction()))
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_delete_member_field_4() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.deleteMembers(List.of(new GroupChange.Actions.DeleteMemberAction()))
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_modify_member_roles_field_5() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.modifyMemberRoles(List.of(new GroupChange.Actions.ModifyMemberRoleAction()))
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_modify_profile_keys_field_6() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.modifyMemberProfileKeys(List.of(new GroupChange.Actions.ModifyMemberProfileKeyAction()))
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_add_pending_members_field_7() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.addPendingMembers(List.of(new GroupChange.Actions.AddPendingMemberAction()))
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_delete_pending_members_field_8() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.deletePendingMembers(List.of(new GroupChange.Actions.DeletePendingMemberAction()))
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_promote_delete_pending_members_field_9() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.promotePendingMembers(List.of(new GroupChange.Actions.PromotePendingMemberAction()))
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_modify_title_field_10() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.modifyTitle(new GroupChange.Actions.ModifyTitleAction())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_modify_avatar_field_11() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.modifyAvatar(new GroupChange.Actions.ModifyAvatarAction())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_modify_disappearing_message_timer_field_12() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.modifyDisappearingMessagesTimer(new GroupChange.Actions.ModifyDisappearingMessagesTimerAction())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_modify_attributes_field_13() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.modifyAttributesAccess(new GroupChange.Actions.ModifyAttributesAccessControlAction())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_modify_member_access_field_14() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.modifyMemberAccess(new GroupChange.Actions.ModifyMembersAccessControlAction())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_modify_add_from_invite_link_field_15() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.modifyAddFromInviteLinkAccess(new GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_add_requesting_members_field_16() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.addRequestingMembers(List.of(new GroupChange.Actions.AddRequestingMemberAction()))
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_delete_requesting_members_field_17() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.deleteRequestingMembers(List.of(new GroupChange.Actions.DeleteRequestingMemberAction()))
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_promote_requesting_members_field_18() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.promoteRequestingMembers(List.of(new GroupChange.Actions.PromoteRequestingMemberAction()))
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_promote_requesting_members_field_19() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.modifyInviteLinkPassword(new GroupChange.Actions.ModifyInviteLinkPasswordAction())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_modify_description_field_20() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.modifyDescription(new GroupChange.Actions.ModifyDescriptionAction())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_modify_description_field_21() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.modifyAnnouncementsOnly(new GroupChange.Actions.ModifyAnnouncementsOnlyAction())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_add_banned_member_field_22() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.addBannedMembers(List.of(new GroupChange.Actions.AddBannedMemberAction()))
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_delete_banned_member_field_23() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.deleteBannedMembers(List.of(new GroupChange.Actions.DeleteBannedMemberAction()))
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_promote_pending_pni_aci_members_field_24() {
GroupChange.Actions actions = new GroupChange.Actions.Builder()
.promotePendingPniAciMembers(List.of(new GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction()))
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
}

View File

@@ -0,0 +1,857 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.junit.Test;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.BannedMember;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.PendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.core.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.util.List;
import java.util.UUID;
import okio.ByteString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.bannedMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.demoteAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encrypt;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encryptedMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encryptedRequestingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMemberRemoval;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingPniAciMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.presentation;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.promoteAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.requestingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class GroupChangeUtil_resolveConflict_Test {
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be resolved by {@link GroupChangeUtil#resolveConflict(DecryptedGroup, DecryptedGroupChange, GroupChange.Actions)}.
*/
@Test
public void ensure_resolveConflict_knows_about_all_fields_of_DecryptedGroupChange() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
24, maxFieldFound);
}
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be resolved by {@link GroupChangeUtil#resolveConflict(DecryptedGroup, DecryptedGroupChange, GroupChange.Actions)}.
*/
@Test
public void ensure_resolveConflict_knows_about_all_fields_of_GroupChange() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + GroupChange.class.getName(),
24, maxFieldFound);
}
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be resolved by {@link GroupChangeUtil#resolveConflict(DecryptedGroup, DecryptedGroupChange, GroupChange.Actions)}.
*/
@Test
public void ensure_resolveConflict_knows_about_all_fields_of_DecryptedGroup() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroup.class, ProtobufTestUtils.IGNORED_DECRYPTED_GROUP_TAGS);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroup.class.getName(),
13, maxFieldFound);
}
@Test
public void empty_actions() {
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(new DecryptedGroup.Builder().build(),
new DecryptedGroupChange.Builder().build(),
new GroupChange.Actions.Builder().build())
.build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_3__changes_to_add_existing_members_are_excluded() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
ProfileKey profileKey2 = randomProfileKey();
DecryptedGroup groupState = new DecryptedGroup.Builder().members(List.of(member(member1), member(member3))).build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder().newMembers(List.of(member(member1), member(member2), member(member3))).build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.addMembers(List.of(new GroupChange.Actions.AddMemberAction.Builder().added(encryptedMember(member1, randomProfileKey())).build(),
new GroupChange.Actions.AddMemberAction.Builder().added(encryptedMember(member2, profileKey2)).build(),
new GroupChange.Actions.AddMemberAction.Builder().added(encryptedMember(member3, randomProfileKey())).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.addMembers(List.of(new GroupChange.Actions.AddMemberAction.Builder().added(encryptedMember(member2, profileKey2)).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_4__changes_to_remove_missing_members_are_excluded() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member2)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.deleteMembers(List.of(UuidUtil.toByteString(member1), UuidUtil.toByteString(member2), UuidUtil.toByteString(member3)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.deleteMembers(List.of(new GroupChange.Actions.DeleteMemberAction.Builder().deletedUserId(encrypt(member1)).build(),
new GroupChange.Actions.DeleteMemberAction.Builder().deletedUserId(encrypt(member2)).build(),
new GroupChange.Actions.DeleteMemberAction.Builder().deletedUserId(encrypt(member3)).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.deleteMembers(List.of(new GroupChange.Actions.DeleteMemberAction.Builder().deletedUserId(encrypt(member2)).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_5__role_change_is_preserved() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(admin(member1), member(member2), member(member3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.modifyMemberRoles(List.of(demoteAdmin(member1), promoteAdmin(member2)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyMemberRoles(List.of(new GroupChange.Actions.ModifyMemberRoleAction.Builder().userId(encrypt(member1)).role(Member.Role.DEFAULT).build(),
new GroupChange.Actions.ModifyMemberRoleAction.Builder().userId(encrypt(member2)).role(Member.Role.ADMINISTRATOR).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertEquals(change, resolvedActions);
}
@Test
public void field_5__unnecessary_role_changes_removed() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
UUID memberNotInGroup = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(admin(member1), member(member2), member(member3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.modifyMemberRoles(List.of(promoteAdmin(member1), promoteAdmin(member2), demoteAdmin(member3), promoteAdmin(memberNotInGroup)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyMemberRoles(List.of(new GroupChange.Actions.ModifyMemberRoleAction.Builder().userId(encrypt(member1)).role(Member.Role.ADMINISTRATOR).build(),
new GroupChange.Actions.ModifyMemberRoleAction.Builder().userId(encrypt(member2)).role(Member.Role.ADMINISTRATOR).build(),
new GroupChange.Actions.ModifyMemberRoleAction.Builder().userId(encrypt(member3)).role(Member.Role.DEFAULT).build(),
new GroupChange.Actions.ModifyMemberRoleAction.Builder().userId(encrypt(memberNotInGroup)).role(Member.Role.ADMINISTRATOR).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.modifyMemberRoles(List.of(new GroupChange.Actions.ModifyMemberRoleAction.Builder().userId(encrypt(member2)).role(Member.Role.ADMINISTRATOR).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_6__profile_key_changes() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
UUID memberNotInGroup = UUID.randomUUID();
ProfileKey profileKey1 = randomProfileKey();
ProfileKey profileKey2 = randomProfileKey();
ProfileKey profileKey3 = randomProfileKey();
ProfileKey profileKey4 = randomProfileKey();
ProfileKey profileKey2b = randomProfileKey();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1, profileKey1), member(member2, profileKey2), member(member3, profileKey3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.modifiedProfileKeys(List.of(member(member1, profileKey1), member(member2, profileKey2b), member(member3, profileKey3), member(memberNotInGroup, profileKey4)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyMemberProfileKeys(List.of(new GroupChange.Actions.ModifyMemberProfileKeyAction.Builder().presentation(presentation(member1, profileKey1)).build(),
new GroupChange.Actions.ModifyMemberProfileKeyAction.Builder().presentation(presentation(member2, profileKey2b)).build(),
new GroupChange.Actions.ModifyMemberProfileKeyAction.Builder().presentation(presentation(member3, profileKey3)).build(),
new GroupChange.Actions.ModifyMemberProfileKeyAction.Builder().presentation(presentation(memberNotInGroup, profileKey4)).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.modifyMemberProfileKeys(List.of(new GroupChange.Actions.ModifyMemberProfileKeyAction.Builder().presentation(presentation(member2, profileKey2b)).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_7__add_pending_members() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
ProfileKey profileKey2 = randomProfileKey();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1)))
.pendingMembers(List.of(pendingMember(member3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newPendingMembers(List.of(pendingMember(member1), pendingMember(member2), pendingMember(member3)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.addPendingMembers(List.of(new GroupChange.Actions.AddPendingMemberAction.Builder().added(new PendingMember.Builder().member(encryptedMember(member1, randomProfileKey())).build()).build(),
new GroupChange.Actions.AddPendingMemberAction.Builder().added(new PendingMember.Builder().member(encryptedMember(member2, profileKey2)).build()).build(),
new GroupChange.Actions.AddPendingMemberAction.Builder().added(new PendingMember.Builder().member(encryptedMember(member3, randomProfileKey())).build()).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.addPendingMembers(List.of(new GroupChange.Actions.AddPendingMemberAction.Builder().added(new PendingMember.Builder().member(encryptedMember(member2, profileKey2)).build()).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_8__delete_pending_members() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1)))
.pendingMembers(List.of(pendingMember(member2)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.deletePendingMembers(List.of(pendingMemberRemoval(member1), pendingMemberRemoval(member2), pendingMemberRemoval(member3)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.deletePendingMembers(List.of(new GroupChange.Actions.DeletePendingMemberAction.Builder().deletedUserId(encrypt(member1)).build(),
new GroupChange.Actions.DeletePendingMemberAction.Builder().deletedUserId(encrypt(member2)).build(),
new GroupChange.Actions.DeletePendingMemberAction.Builder().deletedUserId(encrypt(member3)).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.deletePendingMembers(List.of(new GroupChange.Actions.DeletePendingMemberAction.Builder().deletedUserId(encrypt(member2)).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_9__promote_pending_members() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
ProfileKey profileKey2 = randomProfileKey();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1)))
.pendingMembers(List.of(pendingMember(member2)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.promotePendingMembers(List.of(member(member1), member(member2), member(member3)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.promotePendingMembers(List.of(new GroupChange.Actions.PromotePendingMemberAction.Builder().presentation(presentation(member1, randomProfileKey())).build(),
new GroupChange.Actions.PromotePendingMemberAction.Builder().presentation(presentation(member2, profileKey2)).build(),
new GroupChange.Actions.PromotePendingMemberAction.Builder().presentation(presentation(member3, randomProfileKey())).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.promotePendingMembers(List.of(new GroupChange.Actions.PromotePendingMemberAction.Builder().presentation(presentation(member2, profileKey2)).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_3_to_9__add_of_pending_member_converted_to_a_promote() {
UUID member1 = UUID.randomUUID();
ProfileKey profileKey1 = randomProfileKey();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.pendingMembers(List.of(pendingMember(member1)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newMembers(List.of(member(member1)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.addMembers(List.of(new GroupChange.Actions.AddMemberAction.Builder().added(encryptedMember(member1, profileKey1)).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.promotePendingMembers(List.of(new GroupChange.Actions.PromotePendingMemberAction.Builder().presentation(presentation(member1, profileKey1)).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_10__title_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.title("Existing title")
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newTitle(new DecryptedString.Builder().value_("New title").build())
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyTitle(new GroupChange.Actions.ModifyTitleAction.Builder().title(ByteString.of("New title encrypted".getBytes())).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertEquals(change, resolvedActions);
}
@Test
public void field_10__no_title_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.title("Existing title")
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newTitle(new DecryptedString.Builder().value_("Existing title").build())
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyTitle(new GroupChange.Actions.ModifyTitleAction.Builder().title(ByteString.of("Existing title encrypted".getBytes())).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_11__avatar_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.avatar("Existing avatar")
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newAvatar(new DecryptedString.Builder().value_("New avatar").build())
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyAvatar(new GroupChange.Actions.ModifyAvatarAction.Builder().avatar("New avatar possibly encrypted").build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertEquals(change, resolvedActions);
}
@Test
public void field_11__no_avatar_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.avatar("Existing avatar")
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newAvatar(new DecryptedString.Builder().value_("Existing avatar").build())
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyAvatar(new GroupChange.Actions.ModifyAvatarAction.Builder().avatar("Existing avatar possibly encrypted").build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_12__timer_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.disappearingMessagesTimer(new DecryptedTimer.Builder().duration(123).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newTimer(new DecryptedTimer.Builder().duration(456).build())
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyDisappearingMessagesTimer(new GroupChange.Actions.ModifyDisappearingMessagesTimerAction.Builder().timer(ByteString.EMPTY).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertEquals(change, resolvedActions);
}
@Test
public void field_12__no_timer_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.disappearingMessagesTimer(new DecryptedTimer.Builder().duration(123).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newTimer(new DecryptedTimer.Builder().duration(123).build())
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyDisappearingMessagesTimer(new GroupChange.Actions.ModifyDisappearingMessagesTimerAction.Builder().timer(ByteString.EMPTY).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_13__attribute_access_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().attributes(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newAttributeAccess(AccessControl.AccessRequired.MEMBER)
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyAttributesAccess(new GroupChange.Actions.ModifyAttributesAccessControlAction.Builder().attributesAccess(AccessControl.AccessRequired.MEMBER).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertEquals(change, resolvedActions);
}
@Test
public void field_13__no_attribute_access_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().attributes(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newAttributeAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyAttributesAccess(new GroupChange.Actions.ModifyAttributesAccessControlAction.Builder().attributesAccess(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_14__membership_access_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().members(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newMemberAccess(AccessControl.AccessRequired.MEMBER)
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyMemberAccess(new GroupChange.Actions.ModifyMembersAccessControlAction.Builder().membersAccess(AccessControl.AccessRequired.MEMBER).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertEquals(change, resolvedActions);
}
@Test
public void field_14__no_membership_access_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().members(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyMemberAccess(new GroupChange.Actions.ModifyMembersAccessControlAction.Builder().membersAccess(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_15__no_membership_access_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().addFromInviteLink(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyAddFromInviteLinkAccess(new GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction.Builder().addFromInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_16__changes_to_add_requesting_members_when_full_members_are_removed() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
ProfileKey profileKey2 = randomProfileKey();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1), member(member3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newRequestingMembers(List.of(requestingMember(member1), requestingMember(member2), requestingMember(member3)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.addRequestingMembers(List.of(new GroupChange.Actions.AddRequestingMemberAction.Builder().added(encryptedRequestingMember(member1, randomProfileKey())).build(),
new GroupChange.Actions.AddRequestingMemberAction.Builder().added(encryptedRequestingMember(member2, profileKey2)).build(),
new GroupChange.Actions.AddRequestingMemberAction.Builder().added(encryptedRequestingMember(member3, randomProfileKey())).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.addRequestingMembers(List.of(new GroupChange.Actions.AddRequestingMemberAction.Builder().added(encryptedRequestingMember(member2, profileKey2)).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_16__changes_to_add_requesting_members_when_pending_are_promoted() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
ProfileKey profileKey1 = randomProfileKey();
ProfileKey profileKey2 = randomProfileKey();
ProfileKey profileKey3 = randomProfileKey();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.pendingMembers(List.of(pendingMember(member1), pendingMember(member3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newRequestingMembers(List.of(requestingMember(member1, profileKey1), requestingMember(member2, profileKey2), requestingMember(member3, profileKey3)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.addRequestingMembers(List.of(new GroupChange.Actions.AddRequestingMemberAction.Builder().added(encryptedRequestingMember(member1, profileKey1)).build(),
new GroupChange.Actions.AddRequestingMemberAction.Builder().added(encryptedRequestingMember(member2, profileKey2)).build(),
new GroupChange.Actions.AddRequestingMemberAction.Builder().added(encryptedRequestingMember(member3, profileKey3)).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.promotePendingMembers(List.of(new GroupChange.Actions.PromotePendingMemberAction.Builder().presentation(presentation(member3, profileKey3)).build(),
new GroupChange.Actions.PromotePendingMemberAction.Builder().presentation(presentation(member1, profileKey1)).build()))
.addRequestingMembers(List.of(new GroupChange.Actions.AddRequestingMemberAction.Builder().added(encryptedRequestingMember(member2, profileKey2)).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_17__changes_to_remove_missing_requesting_members_are_excluded() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.requestingMembers(List.of(requestingMember(member2)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.deleteRequestingMembers(List.of(UuidUtil.toByteString(member1), UuidUtil.toByteString(member2), UuidUtil.toByteString(member3)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.deleteRequestingMembers(List.of(new GroupChange.Actions.DeleteRequestingMemberAction.Builder().deletedUserId(encrypt(member1)).build(),
new GroupChange.Actions.DeleteRequestingMemberAction.Builder().deletedUserId(encrypt(member2)).build(),
new GroupChange.Actions.DeleteRequestingMemberAction.Builder().deletedUserId(encrypt(member3)).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.deleteRequestingMembers(List.of(new GroupChange.Actions.DeleteRequestingMemberAction.Builder().deletedUserId(encrypt(member2)).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_18__promote_requesting_members() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1)))
.requestingMembers(List.of(requestingMember(member2)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.promoteRequestingMembers(List.of(approveMember(member1), approveMember(member2), approveMember(member3)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.promoteRequestingMembers(List.of(new GroupChange.Actions.PromoteRequestingMemberAction.Builder().role(Member.Role.DEFAULT).userId(UuidUtil.toByteString(member1)).build(),
new GroupChange.Actions.PromoteRequestingMemberAction.Builder().role(Member.Role.DEFAULT).userId(UuidUtil.toByteString(member2)).build(),
new GroupChange.Actions.PromoteRequestingMemberAction.Builder().role(Member.Role.DEFAULT).userId(UuidUtil.toByteString(member3)).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.promoteRequestingMembers(List.of(new GroupChange.Actions.PromoteRequestingMemberAction.Builder().role(Member.Role.DEFAULT).userId(UuidUtil.toByteString(member2)).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_19__password_change_is_kept() {
ByteString password1 = ByteString.of(Util.getSecretBytes(16));
ByteString password2 = ByteString.of(Util.getSecretBytes(16));
DecryptedGroup groupState = new DecryptedGroup.Builder()
.inviteLinkPassword(password1)
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newInviteLinkPassword(password2)
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyInviteLinkPassword(new GroupChange.Actions.ModifyInviteLinkPasswordAction.Builder().inviteLinkPassword(password2).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.modifyInviteLinkPassword(new GroupChange.Actions.ModifyInviteLinkPasswordAction.Builder().inviteLinkPassword(password2).build())
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_20__description_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.description("Existing title")
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newDescription(new DecryptedString.Builder().value_("New title").build())
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyDescription(new GroupChange.Actions.ModifyDescriptionAction.Builder().description(ByteString.of("New title encrypted".getBytes())).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertEquals(change, resolvedActions);
}
@Test
public void field_20__no_description_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.description("Existing title")
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newDescription(new DecryptedString.Builder().value_("Existing title").build())
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyDescription(new GroupChange.Actions.ModifyDescriptionAction.Builder().description(ByteString.of("Existing title encrypted".getBytes())).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_21__announcement_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.isAnnouncementGroup(EnabledState.DISABLED)
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newIsAnnouncementGroup(EnabledState.ENABLED)
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyAnnouncementsOnly(new GroupChange.Actions.ModifyAnnouncementsOnlyAction.Builder().announcementsOnly(true).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertEquals(change, resolvedActions);
}
@Test
public void field_21__announcement_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.isAnnouncementGroup(EnabledState.ENABLED)
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newIsAnnouncementGroup(EnabledState.ENABLED)
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.modifyAnnouncementsOnly(new GroupChange.Actions.ModifyAnnouncementsOnlyAction.Builder().announcementsOnly(true).build())
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_22__add_banned_members() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1)))
.bannedMembers(List.of(bannedMember(member3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newBannedMembers(List.of(bannedMember(member1), bannedMember(member2), bannedMember(member3)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.addBannedMembers(List.of(new GroupChange.Actions.AddBannedMemberAction.Builder().added(new BannedMember.Builder().userId(encrypt(member1)).build()).build(),
new GroupChange.Actions.AddBannedMemberAction.Builder().added(new BannedMember.Builder().userId(encrypt(member2)).build()).build(),
new GroupChange.Actions.AddBannedMemberAction.Builder().added(new BannedMember.Builder().userId(encrypt(member3)).build()).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.addBannedMembers(List.of(new GroupChange.Actions.AddBannedMemberAction.Builder().added(new BannedMember.Builder().userId(encrypt(member1)).build()).build(),
new GroupChange.Actions.AddBannedMemberAction.Builder().added(new BannedMember.Builder().userId(encrypt(member2)).build()).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_23__delete_banned_members() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1)))
.bannedMembers(List.of(bannedMember(member2)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.deleteBannedMembers(List.of(bannedMember(member1), bannedMember(member2), bannedMember(member3)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.deleteBannedMembers(List.of(new GroupChange.Actions.DeleteBannedMemberAction.Builder().deletedUserId(encrypt(member1)).build(),
new GroupChange.Actions.DeleteBannedMemberAction.Builder().deletedUserId(encrypt(member2)).build(),
new GroupChange.Actions.DeleteBannedMemberAction.Builder().deletedUserId(encrypt(member3)).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.deleteBannedMembers(List.of(new GroupChange.Actions.DeleteBannedMemberAction.Builder().deletedUserId(encrypt(member2)).build()))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_24__promote_pending_members() {
DecryptedMember member1 = pendingPniAciMember(UUID.randomUUID(), UUID.randomUUID(), randomProfileKey());
DecryptedMember member2 = pendingPniAciMember(UUID.randomUUID(), UUID.randomUUID(), randomProfileKey());
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(UuidUtil.fromByteString(member1.aciBytes))))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.promotePendingPniAciMembers(List.of(pendingPniAciMember(member1.aciBytes, member1.pniBytes, member1.profileKey),
pendingPniAciMember(member2.aciBytes, member2.pniBytes, member2.profileKey)))
.build();
GroupChange.Actions change = new GroupChange.Actions.Builder()
.promotePendingPniAciMembers(List.of(new GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction.Builder().presentation(presentation(member1.pniBytes, member1.profileKey)).build(),
new GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction.Builder().presentation(presentation(member2.pniBytes, member2.profileKey)).build()))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = new GroupChange.Actions.Builder()
.promotePendingPniAciMembers(List.of(new GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction.Builder().presentation(presentation(member2.pniBytes, member2.profileKey)).build()))
.build();
assertEquals(expected, resolvedActions);
}
}

View File

@@ -0,0 +1,676 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.junit.Test;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.core.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.util.List;
import java.util.UUID;
import okio.ByteString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.bannedMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.demoteAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMemberRemoval;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingPniAciMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.promoteAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.requestingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test {
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be resolved by {@link GroupChangeUtil#resolveConflict(DecryptedGroup, DecryptedGroupChange)}.
*/
@Test
public void ensure_resolveConflict_knows_about_all_fields_of_DecryptedGroupChange() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
24, maxFieldFound);
}
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be resolved by {@link GroupChangeUtil#resolveConflict(DecryptedGroup, DecryptedGroupChange)}.
*/
@Test
public void ensure_resolveConflict_knows_about_all_fields_of_DecryptedGroup() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroup.class, ProtobufTestUtils.IGNORED_DECRYPTED_GROUP_TAGS);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroup.class.getName(),
13, maxFieldFound);
}
@Test
public void field_3__changes_to_add_existing_members_are_excluded() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1), member(member3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newMembers(List.of(member(member1), member(member2), member(member3)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.newMembers(List.of(member(member2)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_4__changes_to_remove_missing_members_are_excluded() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member2)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.deleteMembers(List.of(UuidUtil.toByteString(member1), UuidUtil.toByteString(member2), UuidUtil.toByteString(member3)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.deleteMembers(List.of(UuidUtil.toByteString(member2)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_5__role_change_is_preserved() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(admin(member1), member(member2), member(member3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.modifyMemberRoles(List.of(demoteAdmin(member1), promoteAdmin(member2)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertEquals(decryptedChange, resolvedChanges);
}
@Test
public void field_5__unnecessary_role_changes_removed() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
UUID memberNotInGroup = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(admin(member1), member(member2), member(member3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.modifyMemberRoles(List.of(promoteAdmin(member1), promoteAdmin(member2), demoteAdmin(member3), promoteAdmin(memberNotInGroup)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.modifyMemberRoles(List.of(promoteAdmin(member2)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_6__profile_key_changes() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
UUID memberNotInGroup = UUID.randomUUID();
ProfileKey profileKey1 = randomProfileKey();
ProfileKey profileKey2 = randomProfileKey();
ProfileKey profileKey3 = randomProfileKey();
ProfileKey profileKey4 = randomProfileKey();
ProfileKey profileKey2b = randomProfileKey();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1, profileKey1), member(member2, profileKey2), member(member3, profileKey3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.modifiedProfileKeys(List.of(member(member1, profileKey1), member(member2, profileKey2b), member(member3, profileKey3), member(memberNotInGroup, profileKey4)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.modifiedProfileKeys(List.of(member(member2, profileKey2b)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_7__add_pending_members() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1)))
.pendingMembers(List.of(pendingMember(member3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newPendingMembers(List.of(pendingMember(member1), pendingMember(member2), pendingMember(member3)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.newPendingMembers(List.of(pendingMember(member2)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_8__delete_pending_members() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1)))
.pendingMembers(List.of(pendingMember(member2)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.deletePendingMembers(List.of(pendingMemberRemoval(member1), pendingMemberRemoval(member2), pendingMemberRemoval(member3)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.deletePendingMembers(List.of(pendingMemberRemoval(member2)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_9__promote_pending_members() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
ProfileKey profileKey2 = randomProfileKey();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1)))
.pendingMembers(List.of(pendingMember(member2)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.promotePendingMembers(List.of(member(member1), member(member2, profileKey2), member(member3)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.promotePendingMembers(List.of(member(member2, profileKey2)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_3_to_9__add_of_pending_member_converted_to_a_promote() {
UUID member1 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.pendingMembers(List.of(pendingMember(member1)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newMembers(List.of(member(member1)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.promotePendingMembers(List.of(member(member1)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_10__title_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.title("Existing title")
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newTitle(new DecryptedString.Builder().value_("New title").build())
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertEquals(decryptedChange, resolvedChanges);
}
@Test
public void field_10__no_title_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.title("Existing title")
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newTitle(new DecryptedString.Builder().value_("Existing title").build())
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertTrue(DecryptedGroupUtil.changeIsEmpty(resolvedChanges));
}
@Test
public void field_11__avatar_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.avatar("Existing avatar")
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newAvatar(new DecryptedString.Builder().value_("New avatar").build())
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertEquals(decryptedChange, resolvedChanges);
}
@Test
public void field_11__no_avatar_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.avatar("Existing avatar")
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newAvatar(new DecryptedString.Builder().value_("Existing avatar").build())
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertTrue(DecryptedGroupUtil.changeIsEmpty(resolvedChanges));
}
@Test
public void field_12__timer_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.disappearingMessagesTimer(new DecryptedTimer.Builder().duration(123).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newTimer(new DecryptedTimer.Builder().duration(456).build())
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertEquals(decryptedChange, resolvedChanges);
}
@Test
public void field_12__no_timer_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.disappearingMessagesTimer(new DecryptedTimer.Builder().duration(123).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newTimer(new DecryptedTimer.Builder().duration(123).build())
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertTrue(DecryptedGroupUtil.changeIsEmpty(resolvedChanges));
}
@Test
public void field_13__attribute_access_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().attributes(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newAttributeAccess(AccessControl.AccessRequired.MEMBER)
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertEquals(decryptedChange, resolvedChanges);
}
@Test
public void field_13__no_attribute_access_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().attributes(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newAttributeAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertTrue(DecryptedGroupUtil.changeIsEmpty(resolvedChanges));
}
@Test
public void field_14__membership_access_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().members(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newMemberAccess(AccessControl.AccessRequired.MEMBER)
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertEquals(decryptedChange, resolvedChanges);
}
@Test
public void field_14__no_membership_access_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().members(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertTrue(DecryptedGroupUtil.changeIsEmpty(resolvedChanges));
}
@Test
public void field_15__no_membership_access_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.accessControl(new AccessControl.Builder().addFromInviteLink(AccessControl.AccessRequired.ADMINISTRATOR).build())
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertTrue(DecryptedGroupUtil.changeIsEmpty(resolvedChanges));
}
@Test
public void field_16__changes_to_add_requesting_members_when_full_members_are_removed() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
ProfileKey profileKey2 = randomProfileKey();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1), member(member3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newRequestingMembers(List.of(requestingMember(member1), requestingMember(member2, profileKey2), requestingMember(member3)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.newRequestingMembers(List.of(requestingMember(member2, profileKey2)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_16__changes_to_add_requesting_members_when_pending_are_promoted() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
ProfileKey profileKey1 = randomProfileKey();
ProfileKey profileKey2 = randomProfileKey();
ProfileKey profileKey3 = randomProfileKey();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.pendingMembers(List.of(pendingMember(member1), pendingMember(member3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newRequestingMembers(List.of(requestingMember(member1, profileKey1), requestingMember(member2, profileKey2), requestingMember(member3, profileKey3)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.promotePendingMembers(List.of(member(member3, profileKey3), member(member1, profileKey1)))
.newRequestingMembers(List.of(requestingMember(member2, profileKey2)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_17__changes_to_remove_missing_requesting_members_are_excluded() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.requestingMembers(List.of(requestingMember(member2)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.deleteRequestingMembers(List.of(UuidUtil.toByteString(member1), UuidUtil.toByteString(member2), UuidUtil.toByteString(member3)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.deleteRequestingMembers(List.of(UuidUtil.toByteString(member2)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_18__promote_requesting_members() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1)))
.requestingMembers(List.of(requestingMember(member2)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.promoteRequestingMembers(List.of(approveMember(member1), approveMember(member2), approveMember(member3)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.promoteRequestingMembers(List.of(approveMember(member2)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_19__password_change_is_kept() {
ByteString password1 = ByteString.of(Util.getSecretBytes(16));
ByteString password2 = ByteString.of(Util.getSecretBytes(16));
DecryptedGroup groupState = new DecryptedGroup.Builder()
.inviteLinkPassword(password1)
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newInviteLinkPassword(password2)
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertEquals(decryptedChange, resolvedChanges);
}
@Test
public void field_20__description_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.description("Existing description")
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newDescription(new DecryptedString.Builder().value_("New description").build())
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertEquals(decryptedChange, resolvedChanges);
}
@Test
public void field_20__no_description_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.description("Existing description")
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newDescription(new DecryptedString.Builder().value_("Existing description").build())
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertTrue(DecryptedGroupUtil.changeIsEmpty(resolvedChanges));
}
@Test
public void field_21__announcement_change_is_preserved() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.isAnnouncementGroup(EnabledState.DISABLED)
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newIsAnnouncementGroup(EnabledState.ENABLED)
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertEquals(decryptedChange, resolvedChanges);
}
@Test
public void field_21__no_announcement_change_is_removed() {
DecryptedGroup groupState = new DecryptedGroup.Builder()
.isAnnouncementGroup(EnabledState.ENABLED)
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newIsAnnouncementGroup(EnabledState.ENABLED)
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertTrue(DecryptedGroupUtil.changeIsEmpty(resolvedChanges));
}
@Test
public void field_22__add_banned_members() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1)))
.bannedMembers(List.of(bannedMember(member3)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.newBannedMembers(List.of(bannedMember(member1), bannedMember(member2), bannedMember(member3)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.newBannedMembers(List.of(bannedMember(member1), bannedMember(member2)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_23__delete_banned_members() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(member1)))
.bannedMembers(List.of(bannedMember(member2)))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.deleteBannedMembers(List.of(bannedMember(member1), bannedMember(member2), bannedMember(member3)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.deleteBannedMembers(List.of(bannedMember(member2)))
.build();
assertEquals(expected, resolvedChanges);
}
@Test
public void field_24__promote_pending_members() {
DecryptedMember member1 = pendingPniAciMember(UUID.randomUUID(), UUID.randomUUID(), randomProfileKey());
DecryptedMember member2 = pendingPniAciMember(UUID.randomUUID(), UUID.randomUUID(), randomProfileKey());
DecryptedGroup groupState = new DecryptedGroup.Builder()
.members(List.of(member(UuidUtil.fromByteString(member1.aciBytes))))
.build();
DecryptedGroupChange decryptedChange = new DecryptedGroupChange.Builder()
.promotePendingPniAciMembers(List.of(pendingPniAciMember(member1.aciBytes, member1.pniBytes, member1.profileKey), pendingPniAciMember(member2.aciBytes, member2.pniBytes, member2.profileKey)))
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
DecryptedGroupChange expected = new DecryptedGroupChange.Builder()
.promotePendingPniAciMembers(List.of(pendingPniAciMember(member2.aciBytes, member2.pniBytes, member2.profileKey)))
.build();
assertEquals(expected, resolvedChanges);
}
}

View File

@@ -0,0 +1,149 @@
package org.whispersystems.signalservice.api.groupsv2
import assertk.assertThat
import assertk.assertions.containsExactly
import assertk.assertions.isEqualTo
import assertk.assertions.single
import org.junit.Before
import org.junit.Test
import org.signal.core.models.ServiceId
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.libsignal.zkgroup.groups.GroupSecretParams
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations.GroupOperations
import org.whispersystems.signalservice.internal.util.Util
import org.whispersystems.signalservice.testutil.LibSignalLibraryUtil
import java.util.UUID
@Suppress("ClassName")
class GroupsV2Operations_ban_Test {
private lateinit var groupOperations: GroupOperations
@Before
fun setup() {
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS()
val server = TestZkGroupServer()
val groupSecretParams = GroupSecretParams.deriveFromMasterKey(GroupMasterKey(Util.getSecretBytes(32)))
val clientZkOperations = ClientZkOperations(server.serverPublicParams)
groupOperations = GroupsV2Operations(clientZkOperations, 10).forGroup(groupSecretParams)
}
@Test
fun addBanToEmptyList() {
val ban = randomACI()
val banUuidsChange = groupOperations.createBanServiceIdsChange(
/* banServiceIds = */
setOf(ban),
/* rejectJoinRequest = */
false,
/* bannedMembersList = */
emptyList()
)
assertThat(banUuidsChange.addBannedMembers)
.single()
.transform { it.added?.userId }
.isEqualTo(groupOperations.encryptServiceId(ban))
}
@Test
fun addBanToPartialFullList() {
val toBan = randomACI()
val alreadyBanned = (0 until 5).map { ProtoTestUtils.bannedMember(UUID.randomUUID()) }
val banUuidsChange = groupOperations.createBanServiceIdsChange(
/* banServiceIds = */
setOf(toBan),
/* rejectJoinRequest = */
false,
/* bannedMembersList = */
alreadyBanned
)
assertThat(banUuidsChange.addBannedMembers)
.single()
.transform { it.added?.userId }
.isEqualTo(groupOperations.encryptServiceId(toBan))
}
@Test
fun addBanToFullList() {
val toBan = ServiceId.ACI.from(UUID.randomUUID())
val alreadyBanned = (0 until 10).map { i ->
ProtoTestUtils.bannedMember(UUID.randomUUID())
.newBuilder()
.timestamp(100L + i)
.build()
}.shuffled()
val banUuidsChange = groupOperations.createBanServiceIdsChange(
/* banServiceIds = */
setOf(toBan),
/* rejectJoinRequest = */
false,
/* bannedMembersList = */
alreadyBanned
)
val oldest = alreadyBanned.minBy { it.timestamp }
assertThat(banUuidsChange.deleteBannedMembers)
.single()
.transform { it.deletedUserId }
.isEqualTo(groupOperations.encryptServiceId(ServiceId.parseOrThrow(oldest.serviceIdBytes)))
assertThat(banUuidsChange.addBannedMembers)
.single()
.transform { it.added?.userId }
.isEqualTo(groupOperations.encryptServiceId(toBan))
}
@Test
fun addMultipleBanToFullList() {
val toBan = (0 until 2).map { ServiceId.ACI.from(UUID.randomUUID()) }
val alreadyBanned = (0 until 10).map { i ->
ProtoTestUtils.bannedMember(UUID.randomUUID())
.newBuilder()
.timestamp(100L + i)
.build()
}.shuffled()
val banUuidsChange = groupOperations.createBanServiceIdsChange(
/* banServiceIds = */
toBan.toMutableSet(),
/* rejectJoinRequest = */
false,
/* bannedMembersList = */
alreadyBanned
)
val oldestTwo = alreadyBanned
.sortedBy { it.timestamp }
.subList(0, 2)
.map { groupOperations.encryptServiceId(ServiceId.parseOrThrow(it.serviceIdBytes)) }
.toTypedArray()
assertThat(banUuidsChange.deleteBannedMembers)
.transform { members ->
members.map { member ->
member.deletedUserId
}
}
.containsExactly(*oldestTwo)
val newBans = (0..1).map { i ->
groupOperations.encryptServiceId(toBan[i])
}.toTypedArray()
assertThat(banUuidsChange.addBannedMembers)
.transform { members ->
members.map { member ->
member.added?.userId
}
}
.containsExactly(*newBans)
}
private fun randomACI(): ServiceId.ACI = ServiceId.ACI.from(UUID.randomUUID())
}

View File

@@ -0,0 +1,527 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.junit.Before;
import org.junit.Test;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialPresentation;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.core.models.ServiceId.ACI;
import org.signal.core.models.ServiceId.PNI;
import org.signal.core.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.testutil.LibSignalLibraryUtil;
import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import okio.ByteString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class GroupsV2Operations_decrypt_change_Test {
private GroupSecretParams groupSecretParams;
private GroupsV2Operations.GroupOperations groupOperations;
private ClientZkOperations clientZkOperations;
private TestZkGroupServer server;
@Before
public void setup() throws InvalidInputException {
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS();
server = new TestZkGroupServer();
groupSecretParams = GroupSecretParams.deriveFromMasterKey(new GroupMasterKey(Util.getSecretBytes(32)));
clientZkOperations = new ClientZkOperations(server.getServerPublicParams());
groupOperations = new GroupsV2Operations(clientZkOperations, 1000).forGroup(groupSecretParams);
}
@Test
public void ensure_GroupV2Operations_decryptChange_knows_about_all_fields_of_DecryptedGroupChange() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupV2Operations#decryptChange and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
24,
maxFieldFound);
}
@Test
public void cannot_decrypt_change_with_epoch_higher_than_known() throws IOException, VerificationFailedException, InvalidGroupStateException {
GroupChange change = new GroupChange.Builder()
.changeEpoch(GroupsV2Operations.HIGHEST_KNOWN_EPOCH + 1)
.build();
Optional<DecryptedGroupChange> decryptedGroupChangeOptional = groupOperations.decryptChange(change, DecryptChangeVerificationMode.alreadyTrusted());
assertFalse(decryptedGroupChangeOptional.isPresent());
}
@Test
public void can_pass_revision_through_encrypt_and_decrypt_methods() {
assertDecryption(new GroupChange.Actions.Builder()
.revision(1),
new DecryptedGroupChange.Builder()
.revision(1));
}
@Test
public void can_decrypt_member_additions_field3() {
ACI self = ACI.from(UUID.randomUUID());
ACI newMember = ACI.from(UUID.randomUUID());
ProfileKey profileKey = newProfileKey();
GroupCandidate groupCandidate = groupCandidate(newMember, profileKey);
assertDecryption(groupOperations.createModifyGroupMembershipChange(Collections.singleton(groupCandidate), Collections.emptySet(), self)
.revision(10),
new DecryptedGroupChange.Builder()
.revision(10)
.newMembers(List.of(new DecryptedMember.Builder()
.role(Member.Role.DEFAULT)
.profileKey(ByteString.of(profileKey.serialize()))
.joinedAtRevision(10)
.aciBytes(newMember.toByteString())
.build())));
}
@Test
public void can_decrypt_member_direct_join_field3() {
ACI newMember = ACI.from(UUID.randomUUID());
ProfileKey profileKey = newProfileKey();
GroupCandidate groupCandidate = groupCandidate(newMember, profileKey);
assertDecryption(groupOperations.createGroupJoinDirect(groupCandidate.getExpiringProfileKeyCredential().get())
.revision(10),
new DecryptedGroupChange.Builder()
.revision(10)
.newMembers(List.of(new DecryptedMember.Builder()
.role(Member.Role.DEFAULT)
.profileKey(ByteString.of(profileKey.serialize()))
.joinedAtRevision(10)
.aciBytes(newMember.toByteString())
.build())));
}
@Test
public void can_decrypt_member_additions_direct_to_admin_field3() {
ACI self = ACI.from(UUID.randomUUID());
ACI newMember = ACI.from(UUID.randomUUID());
ProfileKey profileKey = newProfileKey();
GroupCandidate groupCandidate = groupCandidate(newMember, profileKey);
assertDecryption(groupOperations.createModifyGroupMembershipChange(Collections.singleton(groupCandidate), Collections.emptySet(), self)
.revision(10),
new DecryptedGroupChange.Builder()
.revision(10)
.newMembers(List.of(new DecryptedMember.Builder()
.role(Member.Role.DEFAULT)
.profileKey(ByteString.of(profileKey.serialize()))
.joinedAtRevision(10)
.aciBytes(newMember.toByteString())
.build())));
}
@Test(expected = InvalidGroupStateException.class)
public void cannot_decrypt_member_additions_with_bad_cipher_text_field3() throws IOException, VerificationFailedException, InvalidGroupStateException {
byte[] randomPresentation = Util.getSecretBytes(5);
GroupChange.Actions.Builder actions = new GroupChange.Actions.Builder();
actions.addMembers(List.of(new GroupChange.Actions.AddMemberAction.Builder().added(new Member.Builder().role(Member.Role.DEFAULT)
.presentation(ByteString.of(randomPresentation)).build()).build()));
groupOperations.decryptChange(new GroupChange.Builder().actions(actions.build().encodeByteString()).build(), DecryptChangeVerificationMode.alreadyTrusted());
}
@Test
public void can_decrypt_member_removals_field4() {
ACI oldMember = ACI.from(UUID.randomUUID());
assertDecryption(groupOperations.createRemoveMembersChange(Collections.singleton(oldMember), false, Collections.emptyList())
.revision(10),
new DecryptedGroupChange.Builder()
.revision(10)
.deleteMembers(List.of(oldMember.toByteString())));
}
@Test(expected = InvalidGroupStateException.class)
public void cannot_decrypt_member_removals_with_bad_cipher_text_field4() throws IOException, VerificationFailedException, InvalidGroupStateException {
byte[] randomPresentation = Util.getSecretBytes(5);
GroupChange.Actions.Builder actions = new GroupChange.Actions.Builder();
actions.deleteMembers(List.of(new GroupChange.Actions.DeleteMemberAction.Builder().deletedUserId(ByteString.of(randomPresentation)).build()));
groupOperations.decryptChange(new GroupChange.Builder().actions(actions.build().encodeByteString()).build(), DecryptChangeVerificationMode.alreadyTrusted());
}
@Test
public void can_decrypt_modify_member_action_role_to_admin_field5() {
ACI member = ACI.from(UUID.randomUUID());
assertDecryption(groupOperations.createChangeMemberRole(member, Member.Role.ADMINISTRATOR),
new DecryptedGroupChange.Builder()
.modifyMemberRoles(List.of(new DecryptedModifyMemberRole.Builder()
.aciBytes(member.toByteString())
.role(Member.Role.ADMINISTRATOR)
.build())));
}
@Test
public void can_decrypt_modify_member_action_role_to_member_field5() {
ACI member = ACI.from(UUID.randomUUID());
assertDecryption(groupOperations.createChangeMemberRole(member, Member.Role.DEFAULT),
new DecryptedGroupChange.Builder()
.modifyMemberRoles(List.of(new DecryptedModifyMemberRole.Builder()
.aciBytes(member.toByteString())
.role(Member.Role.DEFAULT).build())));
}
@Test
public void can_decrypt_modify_member_profile_key_action_field6() {
ACI self = ACI.from(UUID.randomUUID());
ProfileKey profileKey = newProfileKey();
GroupCandidate groupCandidate = groupCandidate(self, profileKey);
assertDecryption(groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.getExpiringProfileKeyCredential().get())
.revision(10),
new DecryptedGroupChange.Builder()
.revision(10)
.modifiedProfileKeys(List.of(new DecryptedMember.Builder()
.role(Member.Role.UNKNOWN)
.joinedAtRevision(-1)
.profileKey(ByteString.of(profileKey.serialize()))
.aciBytes(self.toByteString())
.build())));
}
@Test
public void can_decrypt_member_invitations_field7() {
ACI self = ACI.from(UUID.randomUUID());
ACI newMember = ACI.from(UUID.randomUUID());
GroupCandidate groupCandidate = new GroupCandidate(newMember, Optional.empty());
assertDecryption(groupOperations.createModifyGroupMembershipChange(Collections.singleton(groupCandidate), Collections.emptySet(), self)
.revision(13),
new DecryptedGroupChange.Builder()
.revision(13)
.newPendingMembers(List.of(new DecryptedPendingMember.Builder()
.addedByAci(self.toByteString())
.serviceIdCipherText(groupOperations.encryptServiceId(newMember))
.role(Member.Role.DEFAULT)
.serviceIdBytes(newMember.toByteString())
.build())));
}
@Test
public void can_decrypt_pending_member_removals_field8() throws InvalidInputException {
ACI oldMember = ACI.from(UUID.randomUUID());
UuidCiphertext uuidCiphertext = new UuidCiphertext(groupOperations.encryptServiceId(oldMember).toByteArray());
assertDecryption(groupOperations.createRemoveInvitationChange(Collections.singleton(uuidCiphertext)),
new DecryptedGroupChange.Builder()
.deletePendingMembers(List.of(new DecryptedPendingMemberRemoval.Builder()
.serviceIdBytes(oldMember.toByteString())
.serviceIdCipherText(ByteString.of(uuidCiphertext.serialize()))
.build())));
}
@Test
public void can_decrypt_pending_member_removals_with_bad_cipher_text_field8() {
byte[] uuidCiphertext = Util.getSecretBytes(60);
assertDecryption(new GroupChange.Actions.Builder()
.deletePendingMembers(List.of(new GroupChange.Actions.DeletePendingMemberAction.Builder()
.deletedUserId(ByteString.of(uuidCiphertext)).build())),
new DecryptedGroupChange.Builder()
.deletePendingMembers(List.of(new DecryptedPendingMemberRemoval.Builder()
.serviceIdBytes(UuidUtil.toByteString(UuidUtil.UNKNOWN_UUID))
.serviceIdCipherText(ByteString.of(uuidCiphertext))
.build())));
}
@Test
public void can_decrypt_promote_pending_member_field9() {
ACI newMember = ACI.from(UUID.randomUUID());
ProfileKey profileKey = newProfileKey();
GroupCandidate groupCandidate = groupCandidate(newMember, profileKey);
assertDecryption(groupOperations.createAcceptInviteChange(groupCandidate.getExpiringProfileKeyCredential().get()),
new DecryptedGroupChange.Builder()
.promotePendingMembers(List.of(new DecryptedMember.Builder()
.aciBytes(newMember.toByteString())
.role(Member.Role.DEFAULT)
.profileKey(ByteString.of(profileKey.serialize()))
.joinedAtRevision(-1)
.build())));
}
@Test
public void can_decrypt_title_field_10() {
assertDecryption(groupOperations.createModifyGroupTitle("New title"),
new DecryptedGroupChange.Builder()
.newTitle(new DecryptedString.Builder().value_("New title").build()));
}
@Test
public void can_decrypt_avatar_key_field_11() {
assertDecryption(new GroupChange.Actions.Builder()
.modifyAvatar(new GroupChange.Actions.ModifyAvatarAction.Builder().avatar("New avatar").build()),
new DecryptedGroupChange.Builder()
.newAvatar(new DecryptedString.Builder().value_("New avatar").build()));
}
@Test
public void can_decrypt_timer_value_field_12() {
assertDecryption(groupOperations.createModifyGroupTimerChange(100),
new DecryptedGroupChange.Builder()
.newTimer(new DecryptedTimer.Builder().duration(100).build()));
}
@Test
public void can_pass_through_new_attribute_access_rights_field_13() {
assertDecryption(groupOperations.createChangeAttributesRights(AccessControl.AccessRequired.MEMBER),
new DecryptedGroupChange.Builder()
.newAttributeAccess(AccessControl.AccessRequired.MEMBER));
}
@Test
public void can_pass_through_new_membership_rights_field_14() {
assertDecryption(groupOperations.createChangeMembershipRights(AccessControl.AccessRequired.ADMINISTRATOR),
new DecryptedGroupChange.Builder()
.newMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR));
}
@Test
public void can_pass_through_new_add_by_invite_link_rights_field_15() {
assertDecryption(groupOperations.createChangeJoinByLinkRights(AccessControl.AccessRequired.ADMINISTRATOR),
new DecryptedGroupChange.Builder()
.newInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR));
}
@Test
public void can_pass_through_new_add_by_invite_link_rights_field_15_unsatisfiable() {
assertDecryption(groupOperations.createChangeJoinByLinkRights(AccessControl.AccessRequired.UNSATISFIABLE),
new DecryptedGroupChange.Builder()
.newInviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE));
}
@Test
public void can_decrypt_member_requests_field16() {
ACI newRequestingMember = ACI.from(UUID.randomUUID());
ProfileKey profileKey = newProfileKey();
GroupCandidate groupCandidate = groupCandidate(newRequestingMember, profileKey);
assertDecryption(groupOperations.createGroupJoinRequest(groupCandidate.getExpiringProfileKeyCredential().get())
.revision(10),
new DecryptedGroupChange.Builder()
.revision(10)
.newRequestingMembers(List.of(new DecryptedRequestingMember.Builder()
.aciBytes(newRequestingMember.toByteString())
.profileKey(ByteString.of(profileKey.serialize()))
.build())));
}
@Test
public void can_decrypt_member_requests_refusals_field17() {
ACI newRequestingMember = ACI.from(UUID.randomUUID());
assertDecryption(groupOperations.createRefuseGroupJoinRequest(Collections.singleton(newRequestingMember), true, Collections.emptyList())
.revision(10),
new DecryptedGroupChange.Builder()
.revision(10)
.deleteRequestingMembers(List.of(newRequestingMember.toByteString()))
.newBannedMembers(List.of(new DecryptedBannedMember.Builder().serviceIdBytes(newRequestingMember.toByteString()).build())));
}
@Test
public void can_decrypt_promote_requesting_members_field18() {
UUID newRequestingMember = UUID.randomUUID();
assertDecryption(groupOperations.createApproveGroupJoinRequest(Collections.singleton(newRequestingMember))
.revision(15),
new DecryptedGroupChange.Builder()
.revision(15)
.promoteRequestingMembers(List.of(new DecryptedApproveMember.Builder()
.role(Member.Role.DEFAULT)
.aciBytes(UuidUtil.toByteString(newRequestingMember))
.build())));
}
@Test
public void can_pass_through_new_invite_link_password_field19() {
byte[] newPassword = Util.getSecretBytes(16);
assertDecryption(new GroupChange.Actions.Builder()
.modifyInviteLinkPassword(new GroupChange.Actions.ModifyInviteLinkPasswordAction.Builder()
.inviteLinkPassword(ByteString.of(newPassword))
.build()),
new DecryptedGroupChange.Builder()
.newInviteLinkPassword(ByteString.of(newPassword)));
}
@Test
public void can_pass_through_new_description_field20() {
assertDecryption(groupOperations.createModifyGroupDescription("New Description"),
new DecryptedGroupChange.Builder()
.newDescription(new DecryptedString.Builder().value_("New Description").build()));
}
@Test
public void can_pass_through_new_announcment_only_field21() {
assertDecryption(new GroupChange.Actions.Builder()
.modifyAnnouncementsOnly(new GroupChange.Actions.ModifyAnnouncementsOnlyAction.Builder()
.announcementsOnly(true)
.build()),
new DecryptedGroupChange.Builder()
.newIsAnnouncementGroup(EnabledState.ENABLED));
}
@Test
public void can_decrypt_member_bans_field22() {
ACI ban = ACI.from(UUID.randomUUID());
assertDecryption(groupOperations.createBanServiceIdsChange(Collections.singleton(ban), false, Collections.emptyList())
.revision(13),
new DecryptedGroupChange.Builder()
.revision(13)
.newBannedMembers(List.of(new DecryptedBannedMember.Builder()
.serviceIdBytes(ban.toByteString())
.build())));
}
@Test
public void can_decrypt_banned_member_removals_field23() {
ACI ban = ACI.from(UUID.randomUUID());
assertDecryption(groupOperations.createUnbanServiceIdsChange(Collections.singleton(ban))
.revision(13),
new DecryptedGroupChange.Builder()
.revision(13)
.deleteBannedMembers(List.of(new DecryptedBannedMember.Builder()
.serviceIdBytes(ban.toByteString()).build())));
}
@Test
public void can_decrypt_promote_pending_pni_aci_member_field24() {
ACI memberAci = ACI.from(UUID.randomUUID());
PNI memberPni = PNI.from(UUID.randomUUID());
ProfileKey profileKey = newProfileKey();
GroupChange.Actions.Builder builder = new GroupChange.Actions.Builder()
.sourceServiceId(groupOperations.encryptServiceId(memberPni))
.revision(5)
.promotePendingPniAciMembers(List.of(new GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction.Builder()
.userId(groupOperations.encryptServiceId(memberAci))
.pni(groupOperations.encryptServiceId(memberPni))
.profileKey(encryptProfileKey(memberAci, profileKey))
.build()));
assertDecryptionWithEditorSet(builder,
new DecryptedGroupChange.Builder()
.editorServiceIdBytes(memberAci.toByteString())
.revision(5)
.promotePendingPniAciMembers(List.of(new DecryptedMember.Builder()
.aciBytes(memberAci.toByteString())
.pniBytes(memberPni.toByteString())
.role(Member.Role.DEFAULT)
.profileKey(ByteString.of(profileKey.serialize()))
.joinedAtRevision(5)
.build())));
}
private static ProfileKey newProfileKey() {
try {
return new ProfileKey(Util.getSecretBytes(32));
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
private ByteString encryptProfileKey(ACI aci, ProfileKey profileKey) {
return ByteString.of(new ClientZkGroupCipher(groupSecretParams).encryptProfileKey(profileKey, aci.getLibSignalAci()).serialize());
}
static GroupCandidate groupCandidate(UUID uuid) {
return new GroupCandidate(ACI.from(uuid), Optional.empty());
}
GroupCandidate groupCandidate(ACI aci, ProfileKey profileKey) {
try {
ClientZkProfileOperations profileOperations = clientZkOperations.getProfileOperations();
ProfileKeyCommitment commitment = profileKey.getCommitment(aci.getLibSignalAci());
ProfileKeyCredentialRequestContext requestContext = profileOperations.createProfileKeyCredentialRequestContext(aci.getLibSignalAci(), profileKey);
ProfileKeyCredentialRequest request = requestContext.getRequest();
ExpiringProfileKeyCredentialResponse expiringProfileKeyCredentialResponse = server.getExpiringProfileKeyCredentialResponse(request, aci, commitment, Instant.now().plus(7, ChronoUnit.DAYS).truncatedTo(ChronoUnit.DAYS));
ExpiringProfileKeyCredential profileKeyCredential = profileOperations.receiveExpiringProfileKeyCredential(requestContext, expiringProfileKeyCredentialResponse);
GroupCandidate groupCandidate = new GroupCandidate(aci, Optional.of(profileKeyCredential));
ProfileKeyCredentialPresentation presentation = profileOperations.createProfileKeyCredentialPresentation(groupSecretParams, profileKeyCredential);
server.assertProfileKeyCredentialPresentation(groupSecretParams.getPublicParams(), presentation, Instant.now());
return groupCandidate;
} catch (VerificationFailedException e) {
throw new AssertionError(e);
}
}
void assertDecryption(GroupChange.Actions.Builder inputChange,
DecryptedGroupChange.Builder expectedDecrypted)
{
ACI editor = ACI.from(UUID.randomUUID());
assertDecryptionWithEditorSet(inputChange.sourceServiceId(groupOperations.encryptServiceId(editor)), expectedDecrypted.editorServiceIdBytes(editor.toByteString()));
}
void assertDecryptionWithEditorSet(GroupChange.Actions.Builder inputChange,
DecryptedGroupChange.Builder expectedDecrypted)
{
GroupChange.Actions actions = inputChange.build();
GroupChange change = new GroupChange.Builder()
.actions(actions.encodeByteString())
.build();
DecryptedGroupChange decryptedGroupChange = decrypt(change);
assertEquals(expectedDecrypted.build(),
decryptedGroupChange);
}
private DecryptedGroupChange decrypt(GroupChange build) {
try {
return groupOperations.decryptChange(build, DecryptChangeVerificationMode.alreadyTrusted()).get();
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,145 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.junit.Before;
import org.junit.Test;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupJoinInfo;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.testutil.LibSignalLibraryUtil;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class GroupsV2Operations_decrypt_groupJoinInfo_Test {
private GroupsV2Operations.GroupOperations groupOperations;
@Before
public void setup() throws InvalidInputException {
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS();
TestZkGroupServer server = new TestZkGroupServer();
ClientZkOperations clientZkOperations = new ClientZkOperations(server.getServerPublicParams());
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(new GroupMasterKey(Util.getSecretBytes(32)));
groupOperations = new GroupsV2Operations(clientZkOperations, 1000).forGroup(groupSecretParams);
}
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be decrypted by {@link GroupsV2Operations.GroupOperations#decryptGroupJoinInfo}.
*/
@Test
public void ensure_GroupOperations_knows_about_all_fields_of_Group() {
int maxFieldFound = getMaxDeclaredFieldNumber(GroupJoinInfo.class);
assertEquals("GroupOperations and its tests need updating to account for new fields on " + GroupJoinInfo.class.getName(),
8, maxFieldFound);
}
@Test
public void decrypt_title_field_2() {
GroupJoinInfo groupJoinInfo = new GroupJoinInfo.Builder()
.title(groupOperations.encryptTitle("Title!"))
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertEquals("Title!", decryptedGroupJoinInfo.title);
}
@Test
public void avatar_field_passed_through_3() {
GroupJoinInfo groupJoinInfo = new GroupJoinInfo.Builder()
.avatar("AvatarCdnKey")
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertEquals("AvatarCdnKey", decryptedGroupJoinInfo.avatar);
}
@Test
public void member_count_passed_through_4() {
GroupJoinInfo groupJoinInfo = new GroupJoinInfo.Builder()
.memberCount(97)
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertEquals(97, decryptedGroupJoinInfo.memberCount);
}
@Test
public void add_from_invite_link_access_control_passed_though_5_administrator() {
GroupJoinInfo groupJoinInfo = new GroupJoinInfo.Builder()
.addFromInviteLink(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertEquals(AccessControl.AccessRequired.ADMINISTRATOR, decryptedGroupJoinInfo.addFromInviteLink);
}
@Test
public void add_from_invite_link_access_control_passed_though_5_any() {
GroupJoinInfo groupJoinInfo = new GroupJoinInfo.Builder()
.addFromInviteLink(AccessControl.AccessRequired.ANY)
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertEquals(AccessControl.AccessRequired.ANY, decryptedGroupJoinInfo.addFromInviteLink);
}
@Test
public void revision_passed_though_6() {
GroupJoinInfo groupJoinInfo = new GroupJoinInfo.Builder()
.revision(11)
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertEquals(11, decryptedGroupJoinInfo.revision);
}
@Test
public void pending_approval_passed_though_7_true() {
GroupJoinInfo groupJoinInfo = new GroupJoinInfo.Builder()
.pendingAdminApproval(true)
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertTrue(decryptedGroupJoinInfo.pendingAdminApproval);
}
@Test
public void pending_approval_passed_though_7_false() {
GroupJoinInfo groupJoinInfo = new GroupJoinInfo.Builder()
.pendingAdminApproval(false)
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertFalse(decryptedGroupJoinInfo.pendingAdminApproval);
}
@Test
public void decrypt_description_field_8() {
GroupJoinInfo groupJoinInfo = new GroupJoinInfo.Builder()
.description(groupOperations.encryptDescription("Description!"))
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertEquals("Description!", decryptedGroupJoinInfo.description);
}
}

View File

@@ -0,0 +1,324 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.junit.Before;
import org.junit.Test;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.BannedMember;
import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.PendingMember;
import org.signal.storageservice.protos.groups.RequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.core.models.ServiceId.ACI;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.testutil.LibSignalLibraryUtil;
import java.util.List;
import java.util.UUID;
import okio.ByteString;
import static org.junit.Assert.assertEquals;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class GroupsV2Operations_decrypt_group_Test {
private GroupSecretParams groupSecretParams;
private GroupsV2Operations.GroupOperations groupOperations;
@Before
public void setup() throws InvalidInputException {
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS();
TestZkGroupServer server = new TestZkGroupServer();
ClientZkOperations clientZkOperations = new ClientZkOperations(server.getServerPublicParams());
groupSecretParams = GroupSecretParams.deriveFromMasterKey(new GroupMasterKey(Util.getSecretBytes(32)));
groupOperations = new GroupsV2Operations(clientZkOperations, 1000).forGroup(groupSecretParams);
}
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be decrypted by {@link GroupsV2Operations.GroupOperations#decryptGroup}.
*/
@Test
public void ensure_GroupOperations_knows_about_all_fields_of_Group() {
int maxFieldFound = getMaxDeclaredFieldNumber(Group.class);
assertEquals("GroupOperations and its tests need updating to account for new fields on " + Group.class.getName(),
13, maxFieldFound);
}
@Test
public void decrypt_title_field_2() throws VerificationFailedException, InvalidGroupStateException {
Group group = new Group.Builder()
.title(groupOperations.encryptTitle("Title!"))
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals("Title!", decryptedGroup.title);
}
@Test
public void avatar_field_passed_through_3() throws VerificationFailedException, InvalidGroupStateException {
Group group = new Group.Builder()
.avatar("AvatarCdnKey")
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals("AvatarCdnKey", decryptedGroup.avatar);
}
@Test
public void decrypt_message_timer_field_4() throws VerificationFailedException, InvalidGroupStateException {
Group group = new Group.Builder()
.disappearingMessagesTimer(groupOperations.encryptTimer(123))
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(123, decryptedGroup.disappearingMessagesTimer.duration);
}
@Test
public void pass_through_access_control_field_5() throws VerificationFailedException, InvalidGroupStateException {
AccessControl accessControl = new AccessControl.Builder()
.members(AccessControl.AccessRequired.ADMINISTRATOR)
.attributes(AccessControl.AccessRequired.MEMBER)
.addFromInviteLink(AccessControl.AccessRequired.UNSATISFIABLE)
.build();
Group group = new Group.Builder()
.accessControl(accessControl)
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(accessControl, decryptedGroup.accessControl);
}
@Test
public void set_revision_field_6() throws VerificationFailedException, InvalidGroupStateException {
Group group = new Group.Builder()
.revision(99)
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(99, decryptedGroup.revision);
}
@Test
public void decrypt_full_members_field_7() throws VerificationFailedException, InvalidGroupStateException {
ACI admin1 = ACI.from(UUID.randomUUID());
ACI member1 = ACI.from(UUID.randomUUID());
ProfileKey adminProfileKey = newProfileKey();
ProfileKey memberProfileKey = newProfileKey();
Group group = new Group.Builder()
.members(List.of(new Member.Builder()
.role(Member.Role.ADMINISTRATOR)
.userId(groupOperations.encryptServiceId(admin1))
.joinedAtRevision(4)
.profileKey(encryptProfileKey(admin1, adminProfileKey))
.build(),
new Member.Builder()
.role(Member.Role.DEFAULT)
.userId(groupOperations.encryptServiceId(member1))
.joinedAtRevision(7)
.profileKey(encryptProfileKey(member1, memberProfileKey))
.build()))
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(new DecryptedGroup.Builder()
.members(List.of(new DecryptedMember.Builder()
.joinedAtRevision(4)
.aciBytes(admin1.toByteString())
.role(Member.Role.ADMINISTRATOR)
.profileKey(ByteString.of(adminProfileKey.serialize()))
.build(),
new DecryptedMember.Builder()
.joinedAtRevision(7)
.role(Member.Role.DEFAULT)
.aciBytes(member1.toByteString())
.profileKey(ByteString.of(memberProfileKey.serialize()))
.build()))
.build().members,
decryptedGroup.members);
}
@Test
public void decrypt_pending_members_field_8() throws VerificationFailedException, InvalidGroupStateException {
ACI admin1 = ACI.from(UUID.randomUUID());
ACI member1 = ACI.from(UUID.randomUUID());
ACI member2 = ACI.from(UUID.randomUUID());
ACI inviter1 = ACI.from(UUID.randomUUID());
ACI inviter2 = ACI.from(UUID.randomUUID());
Group group = new Group.Builder()
.pendingMembers(List.of(new PendingMember.Builder()
.addedByUserId(groupOperations.encryptServiceId(inviter1))
.timestamp(100)
.member(new Member.Builder()
.role(Member.Role.ADMINISTRATOR)
.userId(groupOperations.encryptServiceId(admin1))
.build())
.build(),
new PendingMember.Builder()
.addedByUserId(groupOperations.encryptServiceId(inviter1))
.timestamp(200)
.member(new Member.Builder()
.role(Member.Role.DEFAULT)
.userId(groupOperations.encryptServiceId(member1))
.build())
.build(),
new PendingMember.Builder()
.addedByUserId(groupOperations.encryptServiceId(inviter2))
.timestamp(1500)
.member(new Member.Builder()
.userId(groupOperations.encryptServiceId(member2)).build())
.build()))
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(new DecryptedGroup.Builder()
.pendingMembers(List.of(new DecryptedPendingMember.Builder()
.serviceIdBytes(admin1.toByteString())
.serviceIdCipherText(groupOperations.encryptServiceId(admin1))
.timestamp(100)
.addedByAci(inviter1.toByteString())
.role(Member.Role.ADMINISTRATOR)
.build(),
new DecryptedPendingMember.Builder()
.serviceIdBytes(member1.toByteString())
.serviceIdCipherText(groupOperations.encryptServiceId(member1))
.timestamp(200)
.addedByAci(inviter1.toByteString())
.role(Member.Role.DEFAULT)
.build(),
new DecryptedPendingMember.Builder()
.serviceIdBytes(member2.toByteString())
.serviceIdCipherText(groupOperations.encryptServiceId(member2))
.timestamp(1500)
.addedByAci(inviter2.toByteString())
.role(Member.Role.DEFAULT)
.build()))
.build()
.pendingMembers,
decryptedGroup.pendingMembers);
}
@Test
public void decrypt_requesting_members_field_9() throws VerificationFailedException, InvalidGroupStateException {
ACI admin1 = ACI.from(UUID.randomUUID());
ACI member1 = ACI.from(UUID.randomUUID());
ProfileKey adminProfileKey = newProfileKey();
ProfileKey memberProfileKey = newProfileKey();
Group group = new Group.Builder()
.requestingMembers(List.of(new RequestingMember.Builder()
.userId(groupOperations.encryptServiceId(admin1))
.profileKey(encryptProfileKey(admin1, adminProfileKey))
.timestamp(5000)
.build(),
new RequestingMember.Builder()
.userId(groupOperations.encryptServiceId(member1))
.profileKey(encryptProfileKey(member1, memberProfileKey))
.timestamp(15000)
.build()))
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(new DecryptedGroup.Builder()
.requestingMembers(List.of(new DecryptedRequestingMember.Builder()
.aciBytes(admin1.toByteString())
.profileKey(ByteString.of(adminProfileKey.serialize()))
.timestamp(5000)
.build(),
new DecryptedRequestingMember.Builder()
.aciBytes(member1.toByteString())
.profileKey(ByteString.of(memberProfileKey.serialize()))
.timestamp(15000)
.build()))
.build()
.requestingMembers,
decryptedGroup.requestingMembers);
}
@Test
public void pass_through_group_link_password_field_10() throws VerificationFailedException, InvalidGroupStateException {
ByteString password = ByteString.of(Util.getSecretBytes(16));
Group group = new Group.Builder()
.inviteLinkPassword(password)
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(password, decryptedGroup.inviteLinkPassword);
}
@Test
public void decrypt_description_field_11() throws VerificationFailedException, InvalidGroupStateException {
Group group = new Group.Builder()
.description(groupOperations.encryptDescription("Description!"))
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals("Description!", decryptedGroup.description);
}
@Test
public void decrypt_announcements_field_12() throws VerificationFailedException, InvalidGroupStateException {
Group group = new Group.Builder()
.announcementsOnly(true)
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(EnabledState.ENABLED, decryptedGroup.isAnnouncementGroup);
}
@Test
public void decrypt_banned_members_field_13() throws VerificationFailedException, InvalidGroupStateException {
ACI member1 = ACI.from(UUID.randomUUID());
Group group = new Group.Builder()
.bannedMembers(List.of(new BannedMember.Builder().userId(groupOperations.encryptServiceId(member1)).build()))
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(1, decryptedGroup.bannedMembers.size());
assertEquals(new DecryptedBannedMember.Builder().serviceIdBytes(member1.toByteString()).build(), decryptedGroup.bannedMembers.get(0));
}
private ByteString encryptProfileKey(ACI aci, ProfileKey profileKey) {
return ByteString.of(new ClientZkGroupCipher(groupSecretParams).encryptProfileKey(profileKey, aci.getLibSignalAci()).serialize());
}
private static ProfileKey newProfileKey() {
try {
return new ProfileKey(Util.getSecretBytes(32));
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,218 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.RequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.core.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.UUID;
import okio.Buffer;
import okio.ByteString;
final class ProtoTestUtils {
static ProfileKey randomProfileKey() {
byte[] contents = new byte[32];
new SecureRandom().nextBytes(contents);
try {
return new ProfileKey(contents);
} catch (InvalidInputException e) {
throw new AssertionError();
}
}
/**
* Emulates encryption by creating a unique {@link ByteString} that won't equal a byte string created from the {@link UUID}.
*/
static ByteString encrypt(UUID uuid) {
byte[] uuidBytes = UuidUtil.toByteArray(uuid);
return ByteString.of(Arrays.copyOf(uuidBytes, uuidBytes.length + 1));
}
/**
* Emulates a presentation by concatenating the uuid and profile key which makes it suitable for
* equality assertions in these tests.
*/
static ByteString presentation(UUID uuid, ProfileKey profileKey) {
byte[] uuidBytes = UuidUtil.toByteArray(uuid);
byte[] profileKeyBytes = profileKey.serialize();
byte[] concat = new byte[uuidBytes.length + profileKeyBytes.length];
System.arraycopy(uuidBytes, 0, concat, 0, uuidBytes.length);
System.arraycopy(profileKeyBytes, 0, concat, uuidBytes.length, profileKeyBytes.length);
return ByteString.of(concat);
}
/**
* Emulates a presentation by concatenating the uuid and profile key which makes it suitable for
* equality assertions in these tests.
*/
static ByteString presentation(ByteString uuid, ByteString profileKey) {
try (Buffer buffer = new Buffer()) {
buffer.write(uuid);
buffer.write(profileKey);
return buffer.readByteString();
}
}
static DecryptedModifyMemberRole promoteAdmin(UUID member) {
return new DecryptedModifyMemberRole.Builder()
.aciBytes(UuidUtil.toByteString(member))
.role(Member.Role.ADMINISTRATOR)
.build();
}
static DecryptedModifyMemberRole demoteAdmin(UUID member) {
return new DecryptedModifyMemberRole.Builder()
.aciBytes(UuidUtil.toByteString(member))
.role(Member.Role.DEFAULT)
.build();
}
static Member encryptedMember(UUID uuid, ProfileKey profileKey) {
return new Member.Builder()
.presentation(presentation(uuid, profileKey))
.build();
}
static RequestingMember encryptedRequestingMember(UUID uuid, ProfileKey profileKey) {
return new RequestingMember.Builder()
.presentation(presentation(uuid, profileKey))
.build();
}
static DecryptedMember member(UUID uuid) {
return new DecryptedMember.Builder()
.aciBytes(UuidUtil.toByteString(uuid))
.role(Member.Role.DEFAULT)
.build();
}
static DecryptedMember member(UUID uuid, ByteString profileKey, int joinedAtRevision) {
return new DecryptedMember.Builder()
.aciBytes(UuidUtil.toByteString(uuid))
.role(Member.Role.DEFAULT)
.joinedAtRevision(joinedAtRevision)
.profileKey(profileKey)
.build();
}
static DecryptedPendingMemberRemoval pendingMemberRemoval(UUID uuid) {
return new DecryptedPendingMemberRemoval.Builder()
.serviceIdBytes(UuidUtil.toByteString(uuid))
.serviceIdCipherText(encrypt(uuid))
.build();
}
static DecryptedPendingMember pendingMember(UUID uuid) {
return new DecryptedPendingMember.Builder()
.serviceIdBytes(UuidUtil.toByteString(uuid))
.serviceIdCipherText(encrypt(uuid))
.role(Member.Role.DEFAULT)
.build();
}
static DecryptedRequestingMember requestingMember(UUID uuid) {
return requestingMember(uuid, newProfileKey());
}
static DecryptedRequestingMember requestingMember(UUID uuid, ProfileKey profileKey) {
return new DecryptedRequestingMember.Builder()
.aciBytes(UuidUtil.toByteString(uuid))
.profileKey(ByteString.of(profileKey.serialize()))
.build();
}
static DecryptedBannedMember bannedMember(UUID uuid) {
return new DecryptedBannedMember.Builder()
.serviceIdBytes(UuidUtil.toByteString(uuid))
.build();
}
static DecryptedApproveMember approveMember(UUID uuid) {
return approve(uuid, Member.Role.DEFAULT);
}
static DecryptedApproveMember approveAdmin(UUID uuid) {
return approve(uuid, Member.Role.ADMINISTRATOR);
}
private static DecryptedApproveMember approve(UUID uuid, Member.Role role) {
return new DecryptedApproveMember.Builder()
.aciBytes(UuidUtil.toByteString(uuid))
.role(role)
.build();
}
static DecryptedMember member(UUID uuid, ProfileKey profileKey) {
return withProfileKey(member(uuid), profileKey);
}
static DecryptedMember pendingPniAciMember(UUID uuid, UUID pni, ProfileKey profileKey) {
return new DecryptedMember.Builder()
.aciBytes(UuidUtil.toByteString(uuid))
.pniBytes(UuidUtil.toByteString(pni))
.profileKey(ByteString.of(profileKey.serialize()))
.build();
}
static DecryptedMember pendingPniAciMember(ByteString uuid, ByteString pni, ByteString profileKey) {
return new DecryptedMember.Builder()
.aciBytes(uuid)
.pniBytes(pni)
.profileKey(profileKey)
.build();
}
static DecryptedMember admin(UUID uuid, ProfileKey profileKey) {
return withProfileKey(admin(uuid), profileKey);
}
static DecryptedMember admin(UUID uuid) {
return new DecryptedMember.Builder()
.aciBytes(UuidUtil.toByteString(uuid))
.role(Member.Role.ADMINISTRATOR)
.build();
}
static DecryptedMember withProfileKey(DecryptedMember member, ProfileKey profileKey) {
return member.newBuilder()
.profileKey(ByteString.of(profileKey.serialize()))
.build();
}
static DecryptedMember asAdmin(DecryptedMember member) {
return new DecryptedMember.Builder()
.aciBytes(member.aciBytes)
.role(Member.Role.ADMINISTRATOR)
.build();
}
static DecryptedMember asMember(DecryptedMember member) {
return new DecryptedMember.Builder()
.aciBytes(member.aciBytes)
.role(Member.Role.DEFAULT)
.build();
}
public static ProfileKey newProfileKey() {
try {
return new ProfileKey(Util.getSecretBytes(32));
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,34 @@
package org.whispersystems.signalservice.api.groupsv2;
import com.squareup.wire.Message;
import com.squareup.wire.WireField;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Stream;
final class ProtobufTestUtils {
/** Tags that should be ignored and not count as part of 'needs support' in the various group decryption tests. */
static final Set<Integer> IGNORED_DECRYPTED_GROUP_TAGS = Collections.singleton(64);
/**
* Finds the largest declared field number in the supplied protobuf class.
*/
static int getMaxDeclaredFieldNumber(Class<? extends Message<?, ?>> protobufClass) {
return getMaxDeclaredFieldNumber(protobufClass, Collections.emptySet());
}
/**
* Finds the largest declared field number in the supplied protobuf class.
*/
static int getMaxDeclaredFieldNumber(Class<? extends Message<?, ?>> protobufClass, Set<Integer> excludeTags) {
return Stream.of(protobufClass.getFields())
.map(f -> f.getAnnotationsByType(WireField.class))
.filter(a -> a.length == 1)
.map(a -> a[0].tag())
.filter(t -> !excludeTags.contains(t))
.max(Integer::compareTo)
.orElse(0);
}
}

View File

@@ -0,0 +1,49 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.libsignal.zkgroup.ServerPublicParams;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.groups.GroupPublicParams;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialPresentation;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest;
import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations;
import org.signal.core.models.ServiceId.ACI;
import org.whispersystems.signalservice.testutil.LibSignalLibraryUtil;
import java.time.Instant;
/**
* Provides Zk group operations that the server would provide.
*/
final class TestZkGroupServer {
private final ServerPublicParams serverPublicParams;
private final ServerZkProfileOperations serverZkProfileOperations;
TestZkGroupServer() {
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS();
ServerSecretParams serverSecretParams = ServerSecretParams.generate();
serverPublicParams = serverSecretParams.getPublicParams();
serverZkProfileOperations = new ServerZkProfileOperations(serverSecretParams);
}
public ServerPublicParams getServerPublicParams() {
return serverPublicParams;
}
public ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse(ProfileKeyCredentialRequest request, ACI aci, ProfileKeyCommitment commitment, Instant expiration) throws VerificationFailedException {
return serverZkProfileOperations.issueExpiringProfileKeyCredential(request, aci.getLibSignalAci(), commitment, expiration);
}
public void assertProfileKeyCredentialPresentation(GroupPublicParams publicParams, ProfileKeyCredentialPresentation profileKeyCredentialPresentation, Instant now) {
try {
serverZkProfileOperations.verifyProfileKeyCredentialPresentation(publicParams, profileKeyCredentialPresentation, now);
} catch (VerificationFailedException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,51 @@
package org.whispersystems.signalservice.api.kbs;
import org.junit.Test;
import org.signal.core.models.MasterKey;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
public final class MasterKeyTest {
@Test(expected = IllegalStateException.class)
public void wrong_length_too_short() {
new MasterKey(new byte[31]);
}
@Test(expected = IllegalStateException.class)
public void wrong_length_too_long() {
new MasterKey(new byte[33]);
}
@Test(expected = NullPointerException.class)
public void invalid_input_null() {
//noinspection ConstantConditions
new MasterKey(null);
}
@Test
public void equality() {
byte[] masterKeyBytes1 = new byte[32];
byte[] masterKeyBytes2 = new byte[32];
MasterKey masterKey1 = new MasterKey(masterKeyBytes1);
MasterKey masterKey2 = new MasterKey(masterKeyBytes2);
assertEquals(masterKey1, masterKey2);
assertEquals(masterKey1.hashCode(), masterKey2.hashCode());
}
@Test
public void in_equality() {
byte[] masterKeyBytes1 = new byte[32];
byte[] masterKeyBytes2 = new byte[32];
masterKeyBytes1[0] = 1;
MasterKey masterKey1 = new MasterKey(masterKeyBytes1);
MasterKey masterKey2 = new MasterKey(masterKeyBytes2);
assertNotEquals(masterKey1, masterKey2);
assertNotEquals(masterKey1.hashCode(), masterKey2.hashCode());
}
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.messages
import okio.ByteString.Companion.toByteString
import org.junit.Test
import org.signal.core.models.ServiceId
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.DataMessage
import org.whispersystems.signalservice.internal.push.Envelope
class EnvelopeContentValidatorTest {
companion object {
private val SELF_ACI = ServiceId.ACI.parseOrThrow("0a5ebe7e-9de7-41a5-a25f-6ace4f8e11d1")
}
@Test
fun `validate - ensure mismatched timestamps are marked invalid`() {
val envelope = Envelope(
timestamp = 1234
)
val content = Content(
dataMessage = DataMessage(
timestamp = 12345
)
)
val result = EnvelopeContentValidator.validate(envelope, content, SELF_ACI)
assert(result is EnvelopeContentValidator.Result.Invalid)
}
@Test
fun `validate - ensure polls without a question are marked invalid`() {
val content = Content(
dataMessage = DataMessage(
pollCreate = DataMessage.PollCreate(
options = listOf("option1", "option2"),
allowMultiple = true
)
)
)
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
assert(result is EnvelopeContentValidator.Result.Invalid)
}
@Test
fun `validate - ensure polls with a question exceeding 100 characters are marked invalid`() {
val content = Content(
dataMessage = DataMessage(
pollCreate = DataMessage.PollCreate(
question = "abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyz",
options = listOf("option1", "option2"),
allowMultiple = true
)
)
)
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
assert(result is EnvelopeContentValidator.Result.Invalid)
}
@Test
fun `validate - ensure polls without at least two options are marked invalid`() {
val content = Content(
dataMessage = DataMessage(
pollCreate = DataMessage.PollCreate(
question = "how are you?",
options = listOf("option1"),
allowMultiple = true
)
)
)
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
assert(result is EnvelopeContentValidator.Result.Invalid)
}
@Test
fun `validate - ensure poll options that exceed 100 characters are marked invalid `() {
val content = Content(
dataMessage = DataMessage(
pollCreate = DataMessage.PollCreate(
question = "how are you",
options = listOf("abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyz", "option2"),
allowMultiple = true
)
)
)
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
assert(result is EnvelopeContentValidator.Result.Invalid)
}
@Test
fun `validate - ensure polls without an explicit allow multiple votes option are marked invalid `() {
val content = Content(
dataMessage = DataMessage(
pollCreate = DataMessage.PollCreate(
question = "how are you",
options = listOf("option1", "option2")
)
)
)
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
assert(result is EnvelopeContentValidator.Result.Invalid)
}
@Test
fun `validate - ensure poll terminate without timestamps are marked invalid `() {
val content = Content(
dataMessage = DataMessage(
pollTerminate = DataMessage.PollTerminate()
)
)
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
assert(result is EnvelopeContentValidator.Result.Invalid)
}
@Test
fun `validate - ensure poll votes without a valid aci are marked invalid`() {
val content = Content(
dataMessage = DataMessage(
pollVote = DataMessage.PollVote(
targetAuthorAciBinary = "bad".toByteArray().toByteString()
)
)
)
val result = EnvelopeContentValidator.validate(Envelope(), content, SELF_ACI)
assert(result is EnvelopeContentValidator.Result.Invalid)
}
}

View File

@@ -0,0 +1,65 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.junit.Test;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.core.models.ServiceId.ACI;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
public class DeviceContactsInputStreamTest {
@Test
public void read() throws IOException, InvalidInputException {
ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
DeviceContactsOutputStream output = new DeviceContactsOutputStream(byteArrayOut, true, true);
Optional<ACI> aciFirst = Optional.of(ACI.from(UUID.randomUUID()));
Optional<String> e164First = Optional.of("+1404555555");
Optional<ACI> aciSecond = Optional.of(ACI.from(UUID.randomUUID()));
Optional<String> e164Second = Optional.of("+1444555555");
DeviceContact first = new DeviceContact(
aciFirst,
e164First,
Optional.of("Teal'c"),
Optional.empty(),
Optional.of(0),
Optional.of(0),
Optional.of(0)
);
DeviceContact second = new DeviceContact(
aciSecond,
e164Second,
Optional.of("Bra'tac"),
Optional.empty(),
Optional.of(0),
Optional.of(0),
Optional.of(0)
);
output.write(first);
output.write(second);
output.close();
ByteArrayInputStream byteArrayIn = new ByteArrayInputStream(byteArrayOut.toByteArray());
DeviceContactsInputStream input = new DeviceContactsInputStream(byteArrayIn);
DeviceContact readFirst = input.read();
DeviceContact readSecond = input.read();
assertEquals(first.getAci(), readFirst.getAci());
assertEquals(first.getE164(), readFirst.getE164());
assertEquals(first.getName(), readFirst.getName());
assertEquals(second.getAci(), readSecond.getAci());
assertEquals(second.getE164(), readSecond.getE164());
assertEquals(second.getName(), readSecond.getName());
}
}

View File

@@ -0,0 +1,64 @@
package org.whispersystems.signalservice.api.payments;
import org.junit.Test;
import java.math.BigDecimal;
import java.util.Currency;
import java.util.Locale;
import static org.junit.Assert.assertEquals;
public class FiatFormatterTest {
private static final Currency javaCurrency = Currency.getInstance("USD");
@Test
public void givenAFiatCurrency_whenIFormatWithDefaultOptions_thenIExpectADefaultFormattedString() {
// GIVEN
Formatter formatter = Formatter.forFiat(javaCurrency, FormatterOptions.defaults(Locale.US));
// WHEN
String result = formatter.format(BigDecimal.valueOf(100));
// THEN
assertEquals("$100.00", result);
}
@Test
public void givenANegative_whenIFormatWithAlwaysPositive_thenIExpectPositive() {
// GIVEN
FormatterOptions options = FormatterOptions.builder(Locale.US).alwaysPositive().build();
Formatter formatter = Formatter.forFiat(javaCurrency, options);
// WHEN
String result = formatter.format(BigDecimal.valueOf(-100L));
// THEN
assertEquals("$100.00", result);
}
@Test
public void givenALargeFiatCurrency_whenIFormatWithDefaultOptions_thenIExpectGrouping() {
// GIVEN
Formatter formatter = Formatter.forFiat(javaCurrency, FormatterOptions.defaults(Locale.US));
// WHEN
String result = formatter.format(BigDecimal.valueOf(1000L));
// THEN
assertEquals("$1,000.00", result);
}
@Test
public void givenAFiatCurrency_whenIFormatWithoutUnit_thenIExpectAStringWithoutUnit() {
// GIVEN
Formatter formatter = Formatter.forFiat(javaCurrency, FormatterOptions.builder(Locale.US).withoutUnit().build());
// WHEN
String result = formatter.format(BigDecimal.ONE);
// THEN
assertEquals("1.00", result);
}
}

View File

@@ -0,0 +1,149 @@
package org.whispersystems.signalservice.api.payments;
import org.junit.Test;
import java.math.BigDecimal;
import java.util.Locale;
import static org.junit.Assert.assertEquals;
public class MobileCoinFormatterTest {
private static final Currency currency = Money.MobileCoin.CURRENCY;
@Test
public void givenAMoneyCurrency_whenIFormatWithDefaultOptions_thenIExpectADefaultFormattedString() {
// GIVEN
Formatter formatter = Formatter.forMoney(currency, FormatterOptions.defaults(Locale.US));
// WHEN
String result = formatter.format(BigDecimal.ONE);
// THEN
assertEquals("1 MOB", result);
}
@Test
public void givenALargeMoneyCurrency_whenIFormatWithDefaultOptions_thenIExpectGrouping() {
// GIVEN
Formatter formatter = Formatter.forMoney(currency, FormatterOptions.defaults(Locale.US));
// WHEN
String result = formatter.format(BigDecimal.valueOf(-1000L));
// THEN
assertEquals("-1,000 MOB", result);
}
@Test
public void givenANegative_whenIFormatWithAlwaysPositive_thenIExpectPositive() {
// GIVEN
FormatterOptions options = FormatterOptions.builder(Locale.US).alwaysPositive().build();
Formatter formatter = Formatter.forMoney(currency, options);
// WHEN
String result = formatter.format(BigDecimal.valueOf(-100L));
// THEN
assertEquals("100 MOB", result);
}
@Test
public void givenAnAmount_whenIFormatWithoutSpaceBeforeUnit_thenIExpectNoSpaceBeforeUnit() {
// GIVEN
FormatterOptions options = FormatterOptions.builder(Locale.US).withoutSpaceBeforeUnit().build();
Formatter formatter = Formatter.forMoney(currency, options);
// WHEN
String result = formatter.format(BigDecimal.valueOf(100L));
// THEN
assertEquals("100MOB", result);
}
@Test
public void givenAnAmount_whenIFormatWithoutUnit_thenIExpectNoSpaceBeforeUnit() {
// GIVEN
FormatterOptions options = FormatterOptions.builder(Locale.US).withoutUnit().build();
Formatter formatter = Formatter.forMoney(currency, options);
// WHEN
String result = formatter.format(BigDecimal.valueOf(100L));
// THEN
assertEquals("100", result);
}
@Test
public void givenAnAmount_whenIFormatWithAlwaysPrefixSign_thenIExpectSignOnPositiveValues() {
// GIVEN
FormatterOptions options = FormatterOptions.builder(Locale.US).alwaysPrefixWithSign().build();
Formatter formatter = Formatter.forMoney(currency, options);
// WHEN
String result = formatter.format(BigDecimal.valueOf(100L));
// THEN
assertEquals("+100 MOB", result);
}
@Test
public void givenAMoneyCurrency_whenIToStringWithDefaultOptions_thenIExpectADefaultFormattedString() {
// GIVEN
FormatterOptions options = FormatterOptions.defaults(Locale.US);
// WHEN
String result = Money.mobileCoin(BigDecimal.ONE).toString(options);
// THEN
assertEquals("1 MOB", result);
}
@Test
public void givenAnAmount_whenIToStringWithAlwaysPrefixSign_thenIExpectSignOnPositiveValues() {
// GIVEN
FormatterOptions options = FormatterOptions.builder(Locale.US).alwaysPrefixWithSign().build();
// WHEN
String result = Money.mobileCoin(BigDecimal.ONE).toString(options);
// THEN
assertEquals("+1 MOB", result);
}
@Test
public void givenAnAmount_whenIToStringWithMaximumFractionalDigitsOf4_thenIExpectRoundingAndTruncating() {
// GIVEN
FormatterOptions options = FormatterOptions.builder(Locale.US).withMaximumFractionDigits(4).build();
// WHEN
String result = Money.mobileCoin(BigDecimal.valueOf(1.1234567)).toString(options);
// THEN
assertEquals("1.1235 MOB", result);
}
@Test
public void givenAnAmount_whenIToStringWithMaximumFractionalDigitsOf5_thenIExpectToSee5Places() {
// GIVEN
FormatterOptions options = FormatterOptions.builder(Locale.US).withMaximumFractionDigits(5).build();
// WHEN
String result = Money.mobileCoin(BigDecimal.valueOf(1.1234507)).toString(options);
// THEN
assertEquals("1.12345 MOB", result);
}
@Test
public void givenAnAmount_whenIToStringWithMaximumFractionalDigitsOf5ButFewerActual_thenIExpectToSeeFewerPlaces() {
// GIVEN
FormatterOptions options = FormatterOptions.builder(Locale.US).withMaximumFractionDigits(5).build();
// WHEN
String result = Money.mobileCoin(BigDecimal.valueOf(1.120003)).toString(options);
// THEN
assertEquals("1.12 MOB", result);
}
}

View File

@@ -0,0 +1,347 @@
package org.whispersystems.signalservice.api.payments;
import org.junit.Test;
import org.whispersystems.signalservice.api.util.Uint64RangeException;
import java.math.BigDecimal;
import java.math.BigInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public final class MoneyTest_MobileCoin {
@Test
public void create_zero() {
Money mobileCoin = Money.mobileCoin(BigDecimal.ZERO);
assertFalse(mobileCoin.isPositive());
assertFalse(mobileCoin.isNegative());
}
@Test
public void create_positive() {
Money mobileCoin = Money.mobileCoin(BigDecimal.ONE);
assertTrue(mobileCoin.isPositive());
assertFalse(mobileCoin.isNegative());
}
@Test
public void create_negative() {
Money mobileCoin = Money.mobileCoin(BigDecimal.ONE.negate());
assertFalse(mobileCoin.isPositive());
assertTrue(mobileCoin.isNegative());
}
@Test
public void toString_format() {
Money.MobileCoin mobileCoin = Money.mobileCoin(BigDecimal.valueOf(-1000.32456));
assertEquals("MOB:-1000324560000000", mobileCoin.toString());
}
@Test
public void toAmountString_format() {
Money.MobileCoin mobileCoin = Money.mobileCoin(BigDecimal.valueOf(-1000.32456));
assertEquals("-1000.32456", mobileCoin.getAmountDecimalString());
}
@Test
public void currency() {
Money mobileCoin = Money.mobileCoin(BigDecimal.valueOf(-1000.32456));
assertEquals("MOB", mobileCoin.getCurrency().getCurrencyCode());
assertEquals(12, mobileCoin.getCurrency().getDecimalPrecision());
}
@Test
public void equality() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
Money mobileCoin10 = Money.mobileCoin(BigDecimal.ONE);
assertEquals(mobileCoin1, mobileCoin10);
assertEquals(mobileCoin1.hashCode(), mobileCoin10.hashCode());
}
@Test
public void inequality() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
Money mobileCoin10 = Money.mobileCoin(BigDecimal.TEN);
assertNotEquals(mobileCoin1, mobileCoin10);
assertNotEquals(mobileCoin1.hashCode(), mobileCoin10.hashCode());
}
@Test
public void negate() {
Money money1 = Money.mobileCoin(BigDecimal.ONE);
Money moneyNegative1 = Money.mobileCoin(BigDecimal.ONE.negate());
Money negated = money1.negate();
assertEquals(moneyNegative1, negated);
}
@Test
public void abs() {
Money money0 = Money.mobileCoin(BigDecimal.ZERO);
Money money1 = Money.mobileCoin(BigDecimal.ONE);
Money moneyNegative1 = Money.mobileCoin(BigDecimal.ONE.negate());
Money absOfZero = money0.abs();
Money absOfPositive = money1.abs();
Money absOfNegative = moneyNegative1.abs();
assertSame(money0, absOfZero);
assertSame(money1, absOfPositive);
assertEquals(money1, absOfNegative);
}
@Test
public void require_cast() {
Money money = Money.mobileCoin(BigDecimal.ONE.negate());
Money.MobileCoin mobileCoin = money.requireMobileCoin();
assertSame(money, mobileCoin);
}
@Test
public void serialize_negative() {
Money.MobileCoin mobileCoin = Money.mobileCoin(BigDecimal.valueOf(-1000.32456));
assertEquals("MOB:-1000324560000000", mobileCoin.serialize());
}
@Test
public void parse_negative() throws Money.ParseException {
Money original = Money.mobileCoin(BigDecimal.valueOf(-1000.32456));
String serialized = original.serialize();
Money deserialized = Money.parse(serialized);
assertEquals(original, deserialized);
}
@Test
public void parseOrThrow() {
Money original = Money.mobileCoin(BigDecimal.valueOf(-123.6323));
String serialized = original.serialize();
Money deserialized = Money.parseOrThrow(serialized);
assertEquals(original, deserialized);
}
@Test
public void parse_zero() {
Money value = Money.parseOrThrow("MOB:0000000000000000");
assertSame(Money.MobileCoin.ZERO, value);
}
@Test(expected = Money.ParseException.class)
public void parse_fail_empty() throws Money.ParseException {
Money.parse("");
}
@Test(expected = Money.ParseException.class)
public void parse_fail_null() throws Money.ParseException {
Money.parse(null);
}
@Test(expected = Money.ParseException.class)
public void parse_fail_unknown_currency() throws Money.ParseException {
Money.parse("XYZ:123");
}
@Test(expected = Money.ParseException.class)
public void parse_fail_no_value() throws Money.ParseException {
Money.parse("MOB");
}
@Test(expected = Money.ParseException.class)
public void parse_fail_too_many_parts() throws Money.ParseException {
Money.parse("MOB:1:2");
}
@Test(expected = AssertionError.class)
public void parseOrThrowOrThrow_fail_empty() {
Money.parseOrThrow("");
}
@Test(expected = AssertionError.class)
public void parseOrThrowOrThrow_fail_null() {
Money.parseOrThrow(null);
}
@Test(expected = AssertionError.class)
public void parseOrThrowOrThrow_fail_unknown_currency() {
Money.parseOrThrow("XYZ:123");
}
@Test(expected = AssertionError.class)
public void parseOrThrowOrThrow_fail_no_value() {
Money.parseOrThrow("MOB");
}
@Test(expected = AssertionError.class)
public void parseOrThrowOrThrow_fail_too_many_parts() {
Money.parseOrThrow("MOB:1:2");
}
@Test
public void from_big_integer_picoMobileCoin() {
Money.MobileCoin mobileCoin1 = Money.mobileCoin(new BigDecimal("352324.325232123456"));
Money.MobileCoin mobileCoin2 = Money.picoMobileCoin(BigInteger.valueOf(352324325232123456L));
assertEquals(mobileCoin1, mobileCoin2);
}
@Test
public void from_big_integer_picoMobileCoin_zero() {
Money.MobileCoin mobileCoin = Money.picoMobileCoin(BigInteger.ZERO);
assertSame(Money.MobileCoin.ZERO, mobileCoin);
}
@Test
public void from_very_large_big_integer_picoMobileCoin() {
Money.MobileCoin mobileCoin1 = Money.mobileCoin(new BigDecimal("352324.325232123456"));
Money.MobileCoin mobileCoin2 = Money.picoMobileCoin(BigInteger.valueOf(352324325232123456L));
assertEquals(mobileCoin1, mobileCoin2);
}
@Test
public void to_picoMob_bigInteger() {
Money.MobileCoin mobileCoin1 = Money.mobileCoin(BigDecimal.valueOf(21324.325232));
BigInteger bigInteger = mobileCoin1.toPicoMobBigInteger();
assertEquals(BigInteger.valueOf(21324325232000000L), bigInteger);
}
@Test(expected = ArithmeticException.class)
public void precision_loss_on_creation() {
Money.mobileCoin(new BigDecimal("10376293.0000000000001"));
}
@Test(expected = ArithmeticException.class)
public void precision_loss_on_creation_negative() {
Money.mobileCoin(new BigDecimal("-10376293.0000000000001"));
}
@Test
public void from_picoMob() {
Money.MobileCoin mobileCoin1 = Money.picoMobileCoin(1234567890987654321L);
assertEquals("1234567.890987654321", mobileCoin1.getAmountDecimalString());
}
@Test
public void to_picoMob() throws Uint64RangeException {
Money.MobileCoin mobileCoin1 = Money.picoMobileCoin(1234567890987654321L);
assertEquals(1234567890987654321L, mobileCoin1.toPicoMobUint64());
}
@Test
public void from_large_picoMob() {
Money.MobileCoin mobileCoin1 = Money.picoMobileCoin(0x9000000000000000L);
assertEquals("10376293.541461622784", mobileCoin1.getAmountDecimalString());
}
@Test
public void from_large_negative() {
Money.MobileCoin mobileCoin1 = Money.mobileCoin(new BigDecimal("-1234567.890987654321"));
assertEquals("-1234567.890987654321", mobileCoin1.getAmountDecimalString());
}
@Test
public void to_large_picoMob() throws Uint64RangeException {
Money.MobileCoin mobileCoin1 = Money.picoMobileCoin(0x9000000000000000L);
assertEquals(0x9000000000000000L, mobileCoin1.toPicoMobUint64());
}
@Test
public void from_maximum_picoMob() {
Money.MobileCoin mobileCoin = Money.picoMobileCoin(0xffffffffffffffffL);
assertEquals("18446744073709551615", mobileCoin.serializeAmountString());
}
@Test
public void from_maximum_picoMob_and_back() throws Uint64RangeException {
Money.MobileCoin mobileCoin = Money.picoMobileCoin(0xffffffffffffffffL);
assertEquals(0xffffffffffffffffL, mobileCoin.toPicoMobUint64());
}
@Test
public void large_mobile_coin_value_exceeding_64_bits() {
Money.MobileCoin mobileCoin = Money.mobileCoin(new BigDecimal("18446744.073709551616"));
assertEquals("18446744073709551616", mobileCoin.serializeAmountString());
}
@Test(expected = Uint64RangeException.class)
public void large_mobile_coin_value_exceeding_64_bits_toPicoMobUint64_failure() throws Uint64RangeException {
Money.MobileCoin mobileCoin = Money.mobileCoin(new BigDecimal("18446744.073709551616"));
mobileCoin.toPicoMobUint64();
}
@Test(expected = Uint64RangeException.class)
public void negative_to_pico_mob_uint64() throws Uint64RangeException {
Money.MobileCoin mobileCoin1 = Money.mobileCoin(new BigDecimal("-1"));
mobileCoin1.toPicoMobUint64();
}
@Test
public void greater_than() {
assertTrue(mobileCoin2(2).greaterThan(mobileCoin2(1)));
assertTrue(mobileCoin2(-1).greaterThan(mobileCoin2(-2)));
assertFalse(mobileCoin2(2).greaterThan(mobileCoin2(2)));
assertFalse(mobileCoin2(1).greaterThan(mobileCoin2(2)));
}
@Test
public void less_than() {
assertTrue(mobileCoin2(1).lessThan(mobileCoin2(2)));
assertTrue(mobileCoin2(-2).lessThan(mobileCoin2(-1)));
assertFalse(mobileCoin2(2).lessThan(mobileCoin2(2)));
assertFalse(mobileCoin2(2).lessThan(mobileCoin2(1)));
}
@Test
public void zero_constant() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ZERO);
Money mobileCoin2 = Money.MobileCoin.ZERO;
assertEquals(mobileCoin1, mobileCoin2);
}
@Test
public void to_zero() {
Money mobileCoin = Money.mobileCoin(BigDecimal.ONE);
assertSame(Money.MobileCoin.ZERO, mobileCoin.toZero());
}
@Test
public void max_long_value() {
assertEquals("MOB:18446744073709551615", Money.MobileCoin.MAX_VALUE.serialize());
}
private static Money.MobileCoin mobileCoin2(double value) {
return Money.mobileCoin(BigDecimal.valueOf(value));
}
}

View File

@@ -0,0 +1,70 @@
package org.whispersystems.signalservice.api.payments;
import org.junit.Test;
import java.math.BigDecimal;
import static org.junit.Assert.assertEquals;
public final class MoneyTest_MobileCoin_add {
@Test
public void add_0() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ZERO);
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ZERO);
Money sum = mobileCoin1.add(mobileCoin2);
assertEquals(Money.mobileCoin(BigDecimal.ZERO), sum);
}
@Test
public void add_1_rhs() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ZERO);
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ONE);
Money sum = mobileCoin1.add(mobileCoin2);
assertEquals(Money.mobileCoin(BigDecimal.ONE), sum);
}
@Test
public void add_1_lhs() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ZERO);
Money sum = mobileCoin1.add(mobileCoin2);
assertEquals(Money.mobileCoin(BigDecimal.ONE), sum);
}
@Test
public void add_2() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ONE);
Money sum = mobileCoin1.add(mobileCoin2);
assertEquals(Money.mobileCoin(BigDecimal.valueOf(2)), sum);
}
@Test
public void add_fraction() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
Money mobileCoin2 = Money.mobileCoin(BigDecimal.valueOf(2.2));
Money sum = mobileCoin1.add(mobileCoin2);
assertEquals(Money.mobileCoin(BigDecimal.valueOf(3.2)), sum);
}
@Test
public void add_negative_fraction() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.valueOf(-5.2));
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ONE);
Money sum = mobileCoin1.add(mobileCoin2);
assertEquals(Money.mobileCoin(BigDecimal.valueOf(-4.2)), sum);
}
}

View File

@@ -0,0 +1,32 @@
package org.whispersystems.signalservice.api.payments;
import org.junit.Test;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static java.util.Arrays.asList;
public final class MoneyTest_MobileCoin_comparators {
@Test
public void sort_ascending() {
Money.MobileCoin mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
Money.MobileCoin mobileCoin2 = Money.mobileCoin(BigDecimal.valueOf(2));
List<Money.MobileCoin> list = asList(mobileCoin2, mobileCoin1);
list.sort(Money.MobileCoin.ASCENDING);
assertEquals(asList(mobileCoin1, mobileCoin2), list);
}
@Test
public void sort_descending() {
Money.MobileCoin mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
Money.MobileCoin mobileCoin2 = Money.mobileCoin(BigDecimal.valueOf(2));
List<Money.MobileCoin> list = asList(mobileCoin1, mobileCoin2);
list.sort(Money.MobileCoin.DESCENDING);
assertEquals(asList(mobileCoin2, mobileCoin1), list);
}
}

View File

@@ -0,0 +1,70 @@
package org.whispersystems.signalservice.api.payments;
import org.junit.Test;
import java.math.BigDecimal;
import static org.junit.Assert.assertEquals;
public final class MoneyTest_MobileCoin_subtract {
@Test
public void subtract_0() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ZERO);
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ZERO);
Money sum = mobileCoin1.subtract(mobileCoin2);
assertEquals(Money.mobileCoin(BigDecimal.ZERO), sum);
}
@Test
public void subtract_1_rhs() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ZERO);
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ONE);
Money sum = mobileCoin1.subtract(mobileCoin2);
assertEquals(Money.mobileCoin(BigDecimal.ONE.negate()), sum);
}
@Test
public void subtract_1_lhs() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ZERO);
Money sum = mobileCoin1.subtract(mobileCoin2);
assertEquals(Money.mobileCoin(BigDecimal.ONE), sum);
}
@Test
public void subtract_2() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ONE);
Money sum = mobileCoin1.subtract(mobileCoin2);
assertEquals(Money.mobileCoin(BigDecimal.ZERO), sum);
}
@Test
public void subtract_fraction() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.valueOf(2.2));
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ONE);
Money sum = mobileCoin1.subtract(mobileCoin2);
assertEquals(Money.mobileCoin(BigDecimal.valueOf(1.2)), sum);
}
@Test
public void subtract_negative_fraction() {
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
Money mobileCoin2 = Money.mobileCoin(BigDecimal.valueOf(-5.2));
Money sum = mobileCoin1.subtract(mobileCoin2);
assertEquals(Money.mobileCoin(BigDecimal.valueOf(6.2)), sum);
}
}

View File

@@ -0,0 +1,50 @@
package org.whispersystems.signalservice.api.payments;
import org.junit.Test;
import java.math.BigDecimal;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
public final class MoneyTest_MobileCoin_sum {
@Test
public void sum_empty_list() {
Money sum = Money.MobileCoin.sum(emptyList());
assertSame(Money.MobileCoin.ZERO, sum);
}
@Test
public void sum_1() {
Money.MobileCoin mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
Money sum = Money.MobileCoin.sum(singletonList(mobileCoin1));
assertSame(mobileCoin1, sum);
}
@Test
public void sum_2() {
Money.MobileCoin mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
Money.MobileCoin mobileCoin2 = Money.mobileCoin(BigDecimal.valueOf(2));
Money sum = Money.MobileCoin.sum(asList(mobileCoin1, mobileCoin2));
assertEquals(Money.mobileCoin(BigDecimal.valueOf(3)), sum);
}
@Test
public void sum_negatives() {
Money.MobileCoin mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
Money.MobileCoin mobileCoin2 = Money.mobileCoin(BigDecimal.valueOf(-2));
Money sum = Money.MobileCoin.sum(asList(mobileCoin1, mobileCoin2));
assertEquals(Money.mobileCoin(BigDecimal.valueOf(-1)), sum);
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.push
import assertk.assertThat
import assertk.assertions.hasSize
import org.junit.Test
import org.whispersystems.signalservice.internal.push.PushTransportDetails
class PushTransportDetailsTest {
private val transportV3 = PushTransportDetails()
@Test
fun testV3Padding() {
(0 until 79).forEach { i ->
val message = ByteArray(i)
assertThat(transportV3.getPaddedMessageBody(message)).hasSize(79)
}
(79 until 159).forEach { i ->
val message = ByteArray(i)
assertThat(transportV3.getPaddedMessageBody(message)).hasSize(159)
}
(159 until 239).forEach { i ->
val message = ByteArray(i)
assertThat(transportV3.getPaddedMessageBody(message)).hasSize(239)
}
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.push
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.signal.core.models.ServiceId
import org.signal.core.models.ServiceId.ACI
import org.signal.core.models.ServiceId.PNI
import org.signal.core.util.UuidUtil
import java.util.UUID
import org.signal.libsignal.protocol.ServiceId.Aci as LibSignalAci
import org.signal.libsignal.protocol.ServiceId.Pni as LibSignalPni
class ServiceIdTests {
@Test
fun `ServiceId parseOrNull String`() {
val uuidString = UUID.randomUUID().toString()
assertNull(ServiceId.parseOrNull(null as String?))
assertNull(ServiceId.parseOrNull(""))
assertNull(ServiceId.parseOrNull("asdf"))
assertEquals(ACI.from(UUID.fromString(uuidString)), ServiceId.parseOrNull(uuidString))
assertEquals(PNI.from(UUID.fromString(uuidString)), ServiceId.parseOrNull("PNI:$uuidString"))
}
@Test
fun `ServiceId parseOrNull ByteArray`() {
val uuid = UUID.randomUUID()
val uuidString = uuid.toString()
val uuidBytes = UuidUtil.toByteArray(uuid)
assertNull(ServiceId.parseOrNull(null as ByteArray?))
assertNull(ServiceId.parseOrNull(ByteArray(0)))
assertNull(ServiceId.parseOrNull(byteArrayOf(1, 2, 3)))
assertEquals(ACI.from(UUID.fromString(uuidString)), ServiceId.parseOrNull(uuidBytes))
assertEquals(PNI.from(UUID.fromString(uuidString)), ServiceId.parseOrNull(LibSignalPni(uuid).toServiceIdBinary()))
assertEquals(ACI.from(UUID.fromString(uuidString)), ServiceId.parseOrNull(LibSignalAci(uuid).toServiceIdFixedWidthBinary()))
assertEquals(PNI.from(UUID.fromString(uuidString)), ServiceId.parseOrNull(LibSignalPni(uuid).toServiceIdFixedWidthBinary()))
}
@Test
fun `ACI parseOrNull String`() {
val uuid = UUID.randomUUID()
val uuidString = uuid.toString()
assertNull(ACI.parseOrNull(null as String?))
assertNull(ACI.parseOrNull(""))
assertNull(ACI.parseOrNull("asdf"))
assertNull(ACI.parseOrNull(LibSignalPni(uuid).toServiceIdString()))
assertEquals(ACI.from(UUID.fromString(uuidString)), ACI.parseOrNull(uuidString))
}
@Test
fun `ACI parseOrNull ByteArray`() {
val uuid = UUID.randomUUID()
val uuidString = uuid.toString()
val uuidBytes = UuidUtil.toByteArray(uuid)
assertNull(ACI.parseOrNull(null as ByteArray?))
assertNull(ACI.parseOrNull(ByteArray(0)))
assertNull(ACI.parseOrNull(byteArrayOf(1, 2, 3)))
assertNull(ACI.parseOrNull(LibSignalPni(uuid).toServiceIdBinary()))
assertEquals(ACI.from(UUID.fromString(uuidString)), ACI.parseOrNull(uuidBytes))
assertEquals(ACI.from(UUID.fromString(uuidString)), ACI.parseOrNull(LibSignalAci(uuid).toServiceIdBinary()))
assertEquals(ACI.from(UUID.fromString(uuidString)), ACI.parseOrNull(LibSignalAci(uuid).toServiceIdFixedWidthBinary()))
}
@Test
fun `PNI parseOrNull String`() {
val uuidString = UUID.randomUUID().toString()
assertNull(PNI.parseOrNull(null as String?))
assertNull(PNI.parseOrNull(""))
assertNull(PNI.parseOrNull("asdf"))
assertEquals(PNI.from(UUID.fromString(uuidString)), PNI.parseOrNull(uuidString))
assertEquals(PNI.from(UUID.fromString(uuidString)), PNI.parseOrNull("PNI:$uuidString"))
}
@Test
fun `PNI parseOrNull ByteArray`() {
val uuid = UUID.randomUUID()
val uuidString = uuid.toString()
val uuidBytes = UuidUtil.toByteArray(uuid)
assertNull(PNI.parseOrNull(null as ByteArray?))
assertNull(PNI.parseOrNull(ByteArray(0)))
assertNull(PNI.parseOrNull(byteArrayOf(1, 2, 3)))
assertNull(PNI.parseOrNull(LibSignalAci(uuid).toServiceIdFixedWidthBinary()))
assertEquals(PNI.from(UUID.fromString(uuidString)), PNI.parseOrNull(uuidBytes))
assertEquals(PNI.from(UUID.fromString(uuidString)), PNI.parseOrNull(LibSignalPni(uuid).toServiceIdBinary()))
}
@Test
fun `PNI parsePrefixedOrNull`() {
val uuidString = UUID.randomUUID().toString()
assertNull(PNI.parsePrefixedOrNull(null))
assertNull(PNI.parsePrefixedOrNull(""))
assertNull(PNI.parsePrefixedOrNull("asdf"))
assertNull(PNI.parsePrefixedOrNull(uuidString))
assertEquals(PNI.from(UUID.fromString(uuidString)), PNI.parsePrefixedOrNull("PNI:$uuidString"))
}
}

View File

@@ -0,0 +1,50 @@
package org.whispersystems.signalservice.api.services
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.donations.DonationsApi
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
class DonationsServiceTest {
private val donationsApi: DonationsApi = mockk<DonationsApi>()
private val testSubject = DonationsService(donationsApi)
private val activeSubscription = ActiveSubscription.EMPTY
@Test
fun givenASubscriberId_whenIGetASuccessfulResponse_thenItIsMappedWithTheCorrectStatusCodeAndNonEmptyObject() {
// GIVEN
val subscriberId = SubscriberId.generate()
every { donationsApi.getSubscription(subscriberId) } returns NetworkResult.Success(activeSubscription)
// WHEN
val response = testSubject.getSubscription(subscriberId)
// THEN
verify { donationsApi.getSubscription(subscriberId) }
assertEquals(200, response.status)
assertTrue(response.result.isPresent)
}
@Test
fun givenASubscriberId_whenIGetAnUnsuccessfulResponse_thenItIsMappedWithTheCorrectStatusCodeAndEmptyObject() {
// GIVEN
val subscriberId = SubscriberId.generate()
every { donationsApi.getSubscription(subscriberId) } returns NetworkResult.StatusCodeError(NonSuccessfulResponseCodeException(403))
// WHEN
val response = testSubject.getSubscription(subscriberId)
// THEN
verify { donationsApi.getSubscription(subscriberId) }
assertEquals(403, response.status)
assertFalse(response.result.isPresent)
}
}

View File

@@ -0,0 +1,62 @@
package org.whispersystems.signalservice.api.storage;
import org.junit.Test;
import org.signal.core.models.ServiceId.ACI;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
import okio.ByteString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
public class SignalContactRecordTest {
private static final ACI ACI_A = ACI.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7");
private static final String E164_A = "+16108675309";
@Test
public void contacts_with_same_identity_key_contents_are_equal() {
byte[] identityKey = new byte[32];
byte[] identityKeyCopy = identityKey.clone();
ContactRecord contactA = contactBuilder(ACI_A, E164_A, "a").identityKey(ByteString.of(identityKey)).build();
ContactRecord contactB = contactBuilder(ACI_A, E164_A, "a").identityKey(ByteString.of(identityKeyCopy)).build();
SignalContactRecord signalContactA = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactA);
SignalContactRecord signalContactB = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactB);
assertEquals(signalContactA, signalContactB);
assertEquals(signalContactA.hashCode(), signalContactB.hashCode());
}
@Test
public void contacts_with_different_identity_key_contents_are_not_equal() {
byte[] identityKey = new byte[32];
byte[] identityKeyCopy = identityKey.clone();
identityKeyCopy[0] = 1;
ContactRecord contactA = contactBuilder(ACI_A, E164_A, "a").identityKey(ByteString.of(identityKey)).build();
ContactRecord contactB = contactBuilder(ACI_A, E164_A, "a").identityKey(ByteString.of(identityKeyCopy)).build();
SignalContactRecord signalContactA = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactA);
SignalContactRecord signalContactB = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactB);
assertNotEquals(signalContactA, signalContactB);
assertNotEquals(signalContactA.hashCode(), signalContactB.hashCode());
}
private static byte[] byteArray(int a) {
byte[] bytes = new byte[4];
bytes[3] = (byte) a;
bytes[2] = (byte)(a >> 8);
bytes[1] = (byte)(a >> 16);
bytes[0] = (byte)(a >> 24);
return bytes;
}
private static ContactRecord.Builder contactBuilder(ACI serviceId, String e164, String givenName) {
return new ContactRecord.Builder()
.e164(e164)
.givenName(givenName);
}
}

View File

@@ -0,0 +1,34 @@
package org.whispersystems.signalservice.api.storage;
import org.junit.Test;
import org.signal.core.models.storageservice.StorageItemKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.whispersystems.signalservice.internal.util.Util;
import static org.junit.Assert.assertArrayEquals;
public class SignalStorageCipherTest {
@Test
public void symmetry() throws InvalidKeyException {
StorageItemKey key = new StorageItemKey(Util.getSecretBytes(32));
byte[] data = Util.getSecretBytes(1337);
byte[] ciphertext = SignalStorageCipher.encrypt(key, data);
byte[] plaintext = SignalStorageCipher.decrypt(key, ciphertext);
assertArrayEquals(data, plaintext);
}
@Test(expected = InvalidKeyException.class)
public void badKeyOnDecrypt() throws InvalidKeyException {
StorageItemKey key = new StorageItemKey(Util.getSecretBytes(32));
byte[] data = Util.getSecretBytes(1337);
byte[] badKey = key.serialize().clone();
badKey[0] += 1;
byte[] ciphertext = SignalStorageCipher.encrypt(key, data);
byte[] plaintext = SignalStorageCipher.decrypt(new StorageItemKey(badKey), ciphertext);
}
}

View File

@@ -0,0 +1,25 @@
package org.whispersystems.signalservice.api.subscriptions;
import org.junit.Test;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ActiveSubscriptionTest {
@Test
public void givenActiveSubscription_whenIIsPaymentFailure_thenIExpectFalse() throws Exception {
String input = "{\"subscription\":{\"level\":2000,\"billingCycleAnchor\":1636124746.000000000,\"endOfCurrentPeriod\":1675609546.000000000,\"active\":true,\"cancelAtPeriodEnd\":false,\"currency\":\"USD\",\"amount\":2000,\"status\":\"active\"},\"chargeFailure\":null}";
ActiveSubscription activeSubscription = JsonUtil.fromJson(input, ActiveSubscription.class);
assertTrue(activeSubscription.isActive());
assertFalse(activeSubscription.isFailedPayment());
}
@Test
public void givenNoActiveSubscription_whenIIsInProgress_thenIExpectFalse() throws Exception {
ActiveSubscription activeSubscription = new ActiveSubscription(null, null);
assertFalse(activeSubscription.isInProgress());
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.util
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import org.signal.core.models.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import java.util.UUID
class CredentialsProviderTest {
private fun makeProvider(aci: UUID?, deviceId: Int = SignalServiceAddress.DEFAULT_DEVICE_ID): CredentialsProvider {
return object : CredentialsProvider {
override fun getAci(): ServiceId.ACI? {
if (aci == null) {
return null
}
return ServiceId.ACI.from(aci)
}
override fun getPni(): ServiceId.PNI {
TODO("Not used")
}
override fun getE164(): String {
TODO("Not used")
}
override fun getDeviceId(): Int {
return deviceId
}
override fun getPassword(): String {
TODO("Not used")
}
}
}
@Test
fun usernameWithDefaultDeviceId() {
val uuid = UUID.randomUUID()
assertEquals(uuid.toString(), makeProvider(uuid).username)
}
@Test
fun usernameWithDeviceId() {
val uuid = UUID.randomUUID()
assertEquals("$uuid.42", makeProvider(uuid, 42).username)
}
@Test
fun usernameWithNullAci() {
assertThrows(NullPointerException::class.java) { makeProvider(aci = null).username }
}
}

View File

@@ -0,0 +1,73 @@
package org.whispersystems.signalservice.api.util;
import org.junit.Test;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public final class OptionalUtilTest {
@Test
public void absent_are_equal() {
assertTrue(OptionalUtil.byteArrayEquals(Optional.empty(), Optional.empty()));
}
@Test
public void first_non_absent_not_equal() {
assertFalse(OptionalUtil.byteArrayEquals(Optional.of(new byte[1]), Optional.empty()));
}
@Test
public void second_non_absent_not_equal() {
assertFalse(OptionalUtil.byteArrayEquals(Optional.empty(), Optional.of(new byte[1])));
}
@Test
public void equal_contents() {
byte[] contentsA = new byte[]{1, 2, 3};
byte[] contentsB = contentsA.clone();
Optional<byte[]> a = Optional.of(contentsA);
Optional<byte[]> b = Optional.of(contentsB);
assertTrue(OptionalUtil.byteArrayEquals(a, b));
assertEquals(OptionalUtil.byteArrayHashCode(a), OptionalUtil.byteArrayHashCode(b));
}
@Test
public void in_equal_contents() {
byte[] contentsA = new byte[]{1, 2, 3};
byte[] contentsB = new byte[]{4, 5, 6};
Optional<byte[]> a = Optional.of(contentsA);
Optional<byte[]> b = Optional.of(contentsB);
assertFalse(OptionalUtil.byteArrayEquals(a, b));
assertNotEquals(OptionalUtil.byteArrayHashCode(a), OptionalUtil.byteArrayHashCode(b));
}
@Test
public void hash_code_absent() {
assertEquals(0, OptionalUtil.byteArrayHashCode(Optional.empty()));
}
@Test
public void or_singleAbsent() {
assertFalse(OptionalUtil.or(Optional.empty()).isPresent());
}
@Test
public void or_multipleAbsent() {
assertFalse(OptionalUtil.or(Optional.empty(), Optional.empty()).isPresent());
}
@Test
public void or_firstHasValue() {
assertEquals(5, OptionalUtil.or(Optional.of(5), Optional.empty()).get().longValue());
}
@Test
public void or_secondHasValue() {
assertEquals(5, OptionalUtil.or(Optional.empty(), Optional.of(5)).get().longValue());
}
}

View File

@@ -0,0 +1,95 @@
package org.whispersystems.signalservice.api.util;
import org.junit.Test;
import java.math.BigInteger;
import static org.junit.Assert.assertEquals;
import static org.whispersystems.signalservice.api.util.Uint64Util.bigIntegerToUInt64;
import static org.whispersystems.signalservice.api.util.Uint64Util.uint64ToBigInteger;
public final class Uint64UtilTest {
@Test
public void long_zero_to_bigInteger() {
BigInteger bigInteger = uint64ToBigInteger(0);
assertEquals("0", bigInteger.toString());
}
@Test
public void long_to_bigInteger() {
BigInteger bigInteger = uint64ToBigInteger(12345L);
assertEquals("12345", bigInteger.toString());
}
@Test
public void bigInteger_zero_to_long() throws Uint64RangeException {
long uint64 = bigIntegerToUInt64(BigInteger.ZERO);
assertEquals(0, uint64);
}
@Test
public void first_uint64_value_to_bigInteger() {
BigInteger bigInteger = uint64ToBigInteger(0x8000000000000000L);
assertEquals("9223372036854775808", bigInteger.toString());
}
@Test
public void bigInteger_to_first_uint64_value() throws Uint64RangeException {
long uint64 = bigIntegerToUInt64(new BigInteger("9223372036854775808"));
assertEquals(0x8000000000000000L, uint64);
}
@Test
public void large_uint64_value_to_bigInteger() {
BigInteger bigInteger = uint64ToBigInteger(0xa523f21e412c14d2L);
assertEquals("11899620852199331026", bigInteger.toString());
}
@Test
public void bigInteger_to_large_uint64_value() throws Uint64RangeException {
long uint64 = bigIntegerToUInt64(new BigInteger("11899620852199331026"));
assertEquals(0xa523f21e412c14d2L, uint64);
}
@Test
public void largest_uint64_value_to_bigInteger() {
BigInteger bigInteger = uint64ToBigInteger(0xffffffffffffffffL);
assertEquals("18446744073709551615", bigInteger.toString());
}
@Test
public void bigInteger_to_largest_uint64_value() throws Uint64RangeException {
long uint64 = bigIntegerToUInt64(new BigInteger("18446744073709551615"));
assertEquals(0xffffffffffffffffL, uint64);
}
@Test(expected = Uint64RangeException.class)
public void too_big_by_one() throws Uint64RangeException {
bigIntegerToUInt64(new BigInteger("18446744073709551616"));
}
@Test(expected = Uint64RangeException.class)
public void too_small_by_one() throws Uint64RangeException {
bigIntegerToUInt64(new BigInteger("-1"));
}
@Test(expected = Uint64RangeException.class)
public void too_big_by_a_lot() throws Uint64RangeException {
bigIntegerToUInt64(new BigInteger("1844674407370955161623"));
}
@Test(expected = Uint64RangeException.class)
public void too_small_by_a_lot() throws Uint64RangeException {
bigIntegerToUInt64(new BigInteger("-1844674407370955161623"));
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.internal.crypto
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Test
import org.signal.core.util.StreamUtil
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
class PaddingInputStreamTest {
/**
* Small stress test to confirm padding input only returns the source stream data
* followed strictly by zeros.
*/
@Test
fun stressTest() {
(0..2048).forEach { length ->
val source = ByteArray(length).apply { fill(42) }
val sourceInput = ByteArrayInputStream(source)
val paddingInput = PaddingInputStream(sourceInput, length.toLong())
val paddedData = ByteArrayOutputStream().let {
StreamUtil.copy(paddingInput, it)
it.toByteArray()
}
paddedData.forEachIndexed { index, byte ->
if (index < length) {
assertThat(byte).isEqualTo(source[index])
} else {
assertThat(byte).isEqualTo(0x00)
}
}
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.internal.crypto
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.junit.Test
import org.signal.core.util.UuidUtil
import org.signal.core.util.toByteArray
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.ecc.ECPrivateKey
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.whispersystems.signalservice.internal.push.ProvisionEnvelope
import org.whispersystems.signalservice.internal.push.ProvisionMessage
import org.whispersystems.signalservice.internal.push.ProvisioningVersion
import java.util.UUID
import kotlin.random.Random
class SecondaryProvisioningCipherTest {
@Test
fun decrypt() {
val provisioningCipher = SecondaryProvisioningCipher.generate(IdentityKeyPair.generate())
val primaryIdentityKeyPair = IdentityKeyPair.generate()
val primaryProfileKey = generateProfileKey()
val primaryProvisioningCipher = PrimaryProvisioningCipher(provisioningCipher.secondaryDevicePublicKey.publicKey)
val aci = UUID.randomUUID()
val message = ProvisionMessage(
aciIdentityKeyPublic = ByteString.of(*primaryIdentityKeyPair.publicKey.serialize()),
aciIdentityKeyPrivate = ByteString.of(*primaryIdentityKeyPair.privateKey.serialize()),
provisioningCode = "code",
provisioningVersion = ProvisioningVersion.CURRENT.value,
number = "+14045555555",
aci = aci.toString(),
profileKey = ByteString.of(*primaryProfileKey.serialize()),
readReceipts = true,
aciBinary = aci.toByteArray().toByteString()
)
val provisionMessage = ProvisionEnvelope.ADAPTER.decode(primaryProvisioningCipher.encrypt(message))
val result = provisioningCipher.decrypt(provisionMessage)
assertThat(result).isInstanceOf<SecondaryProvisioningCipher.ProvisioningDecryptResult.Success<ProvisionMessage>>()
val success = result as SecondaryProvisioningCipher.ProvisioningDecryptResult.Success<ProvisionMessage>
assertThat(message.aci).isEqualTo(UuidUtil.parseOrThrow(success.message.aci!!).toString())
assertThat(message.number).isEqualTo(success.message.number)
assertThat(primaryIdentityKeyPair.serialize()).isEqualTo(IdentityKeyPair(IdentityKey(success.message.aciIdentityKeyPublic!!.toByteArray()), ECPrivateKey(success.message.aciIdentityKeyPrivate!!.toByteArray())).serialize())
assertThat(primaryProfileKey.serialize()).isEqualTo(ProfileKey(success.message.profileKey!!.toByteArray()).serialize())
assertThat(message.readReceipts).isEqualTo(success.message.readReceipts == true)
assertThat(message.userAgent).isEqualTo(success.message.userAgent)
assertThat(message.provisioningCode).isEqualTo(success.message.provisioningCode!!)
assertThat(message.provisioningVersion).isEqualTo(success.message.provisioningVersion!!)
assertThat(message.aciBinary).isEqualTo(UuidUtil.parseOrThrow(success.message.aciBinary!!).toByteArray().toByteString())
}
companion object {
fun generateProfileKey(): ProfileKey {
return ProfileKey(Random.nextBytes(32))
}
}
}

View File

@@ -0,0 +1,41 @@
package org.whispersystems.signalservice.internal.push
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
class ContentRange_parse_Test(
private val input: String?,
private val expectedRangeStart: Int,
private val expectedRangeEnd: Int,
private val expectedSize: Int
) {
@Test
fun rangeStart() {
assertThat(ContentRange.parse(input).get().rangeStart).isEqualTo(expectedRangeStart)
}
@Test
fun rangeEnd() {
assertThat(ContentRange.parse(input).get().rangeEnd).isEqualTo(expectedRangeEnd)
}
@Test
fun totalSize() {
assertThat(ContentRange.parse(input).get().totalSize).isEqualTo(expectedSize)
}
companion object {
@JvmStatic
@Parameterized.Parameters(name = "Content-Range: \"{0}\"")
fun data(): Collection<Array<Any>> {
return listOf(
arrayOf("versions 1-2/3", 1, 2, 3),
arrayOf("versions 23-45/67", 23, 45, 67)
)
}
}
}

View File

@@ -0,0 +1,30 @@
package org.whispersystems.signalservice.internal.push
import assertk.assertThat
import assertk.assertions.isFalse
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
class ContentRange_parse_withInvalidStrings_Test(private val input: String?) {
@Test
fun parse_should_be_absent() {
assertThat(ContentRange.parse(input).isPresent).isFalse()
}
companion object {
@JvmStatic
@Parameterized.Parameters(name = "Content-Range: \"{0}\"")
fun data(): List<Array<String?>> {
return listOf(
arrayOf(null),
arrayOf(""),
arrayOf("23-45/67"),
arrayOf("ersions 23-45/67"),
arrayOf("versions 23-45"),
arrayOf("versions a-b/c")
)
}
}
}

View File

@@ -0,0 +1,43 @@
package org.whispersystems.signalservice.internal.push
import assertk.assertThat
import assertk.assertions.containsExactly
import assertk.assertions.hasSize
import assertk.assertions.isEqualTo
import org.junit.Test
import org.whispersystems.signalservice.internal.util.JsonUtil
class GroupMismatchedDevicesTest {
@Test
fun testSimpleParse() {
val json = """
[
{
"uuid": "12345678-1234-1234-1234-123456789012",
"devices": {
"missingDevices": [1, 2],
"extraDevices": [3]
}
},
{
"uuid": "22345678-1234-1234-1234-123456789012",
"devices": {
"missingDevices": [],
"extraDevices": [2]
}
}
]
""".trimIndent()
val parsed = JsonUtil.fromJson(json, Array<GroupMismatchedDevices>::class.java)
assertThat(parsed).hasSize(2)
val (first, second) = parsed
assertThat(first.uuid).isEqualTo("12345678-1234-1234-1234-123456789012")
assertThat(first.devices.missingDevices).containsExactly(1, 2)
assertThat(first.devices.extraDevices).containsExactly(3)
assertThat(second.uuid).isEqualTo("22345678-1234-1234-1234-123456789012")
assertThat(second.devices.extraDevices).containsExactly(2)
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.internal.push
import assertk.assertThat
import assertk.assertions.containsExactly
import assertk.assertions.hasSize
import assertk.assertions.isEqualTo
import org.junit.Test
import org.whispersystems.signalservice.internal.util.JsonUtil
class GroupStaleDevicesTest {
@Test
fun testSimpleParse() {
val json = """
[
{
"uuid": "12345678-1234-1234-1234-123456789012",
"devices": {
"staleDevices": [3]
}
},
{
"uuid": "22345678-1234-1234-1234-123456789012",
"devices": {
"staleDevices": [2]
}
}
]
""".trimIndent()
val parsed: Array<GroupStaleDevices> = JsonUtil.fromJson(json, Array<GroupStaleDevices>::class.java)
assertThat(parsed).hasSize(2)
val (first, second) = parsed
assertThat(first.uuid).isEqualTo("12345678-1234-1234-1234-123456789012")
assertThat(first.devices.staleDevices).containsExactly(3)
assertThat(second.uuid).isEqualTo("22345678-1234-1234-1234-123456789012")
assertThat(second.devices.staleDevices).containsExactly(2)
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.internal.push.exceptions
import org.junit.Assert.assertEquals
import org.junit.Test
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.internal.util.JsonUtil
class InAppPaymentProcessorErrorTest {
companion object {
private val TEST_PROCESSOR = ActiveSubscription.Processor.STRIPE
private const val TEST_CODE = "account_closed"
private const val TEST_MESSAGE = "test_message"
private const val TEST_OUTCOME_NETWORK_STATUS = "test_outcomeNetworkStatus"
private const val TEST_OUTCOME_REASON = "test_outcomeReason"
private const val TEST_OUTCOME_TYPE = "test_outcomeType"
private val TEST_JSON = """
{
"processor": "${TEST_PROCESSOR.code}",
"chargeFailure": {
"code": "$TEST_CODE",
"message": "$TEST_MESSAGE",
"outcomeNetworkStatus": "$TEST_OUTCOME_NETWORK_STATUS",
"outcomeReason": "$TEST_OUTCOME_REASON",
"outcomeType": "$TEST_OUTCOME_TYPE"
}
}
""".trimIndent()
}
@Test
fun givenTestJson_whenIFromJson_thenIExpectProperlyParsedError() {
val result = JsonUtil.fromJson(TEST_JSON, InAppPaymentProcessorError::class.java)
assertEquals(TEST_PROCESSOR, result.processor)
assertEquals(TEST_CODE, result.chargeFailure.code)
assertEquals(TEST_OUTCOME_TYPE, result.chargeFailure.outcomeType)
assertEquals(TEST_OUTCOME_NETWORK_STATUS, result.chargeFailure.outcomeNetworkStatus)
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.internal.push.http
import okio.Buffer
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
import org.whispersystems.signalservice.api.messages.AttachmentTransferProgress
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
import org.whispersystems.signalservice.internal.util.Util
import java.io.ByteArrayInputStream
class DigestingRequestBodyTest {
private val attachmentKey = Util.getSecretBytes(64)
private val attachmentIV = Util.getSecretBytes(16)
private val input = Util.getSecretBytes(CONTENT_LENGTH)
private val outputStreamFactory = AttachmentCipherOutputStreamFactory(attachmentKey, attachmentIV)
@Test
fun givenSameKeyAndIV_whenIWriteToBuffer_thenIExpectSameDigests() {
val fromStart = getBody(0)
val fromMiddle = getBody(CONTENT_LENGTH / 2L)
Buffer().use { buffer ->
fromStart.writeTo(buffer)
}
Buffer().use { buffer ->
fromMiddle.writeTo(buffer)
}
val fullResult = fromStart.attachmentDigest
assertNotNull(fullResult)
val partialResult = fromMiddle.attachmentDigest
assertNotNull(partialResult)
assertArrayEquals(fullResult?.digest, partialResult?.digest)
assertArrayEquals(fullResult?.incrementalDigest, partialResult?.incrementalDigest)
}
@Test
fun givenSameKeyAndIV_whenIWriteToBuffer_thenIExpectSameContents() {
val fromStart = getBody(0)
val fromMiddle = getBody(CONTENT_LENGTH / 2L)
val cipher1: ByteArray
Buffer().use { buffer ->
fromStart.writeTo(buffer)
cipher1 = buffer.readByteArray()
}
val cipher2: ByteArray
Buffer().use { buffer ->
fromMiddle.writeTo(buffer)
cipher2 = buffer.readByteArray()
}
assertEquals(cipher1.size, TOTAL_LENGTH)
assertEquals(cipher2.size, TOTAL_LENGTH - (CONTENT_LENGTH / 2))
cipher2.indices.forEach { i ->
assertEquals(cipher2[i], cipher1[i + (CONTENT_LENGTH / 2)])
}
}
private fun getBody(contentStart: Long): DigestingRequestBody {
return DigestingRequestBody(
inputStream = ByteArrayInputStream(input),
outputStreamFactory = outputStreamFactory,
contentType = "application/octet",
contentLength = CONTENT_LENGTH.toLong(),
incremental = false,
progressListener = object : SignalServiceAttachment.ProgressListener {
override fun onAttachmentProgress(progress: AttachmentTransferProgress) {
// no-op
}
override fun shouldCancel() = false
},
cancelationSignal = { false },
contentStart = contentStart
)
}
companion object {
private const val CONTENT_LENGTH = 70_000
private val TOTAL_LENGTH = AttachmentCipherStreamUtil.getCiphertextLength(CONTENT_LENGTH.toLong()).toInt()
}
}

View File

@@ -0,0 +1,602 @@
package org.whispersystems.signalservice.internal.websocket
import io.mockk.clearAllMocks
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.reactivex.rxjava3.observers.TestObserver
import okio.ByteString.Companion.toByteString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.signal.libsignal.internal.CompletableFuture
import org.signal.libsignal.net.ChatConnection
import org.signal.libsignal.net.ChatConnectionListener
import org.signal.libsignal.net.ChatServiceException
import org.signal.libsignal.net.Network
import org.signal.libsignal.net.UnauthenticatedChatConnection
import org.whispersystems.signalservice.api.websocket.HealthMonitor
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
class LibSignalChatConnectionTest {
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
private val healthMonitor = mockk<HealthMonitor>()
private val network = mockk<Network>()
private val connection = LibSignalChatConnection("test", network, null, false, healthMonitor)
private val chatConnection = mockk<UnauthenticatedChatConnection>()
private var chatListener: ChatConnectionListener? = null
// Used by default-success mocks for ChatConnection behavior.
private var connectLatch: CountDownLatch? = null
private var disconnectLatch: CountDownLatch? = null
private var sendLatch: CountDownLatch? = null
private fun setupConnectedConnection() {
connectLatch = CountDownLatch(1)
connection.connect()
connectLatch!!.await(100, TimeUnit.MILLISECONDS)
}
@Before
fun before() {
clearAllMocks()
every { healthMonitor.onMessageError(any(), any()) }
every { healthMonitor.onKeepAliveResponse(any(), any()) }
// NB: We provide default success behavior mocks here to cut down on boilerplate later, but it is
// expected that some tests will override some of these to test failures.
//
// We provide a null credentials provider when creating `connection`, so LibSignalChatConnection
// should always call connectUnauthChat()
// TODO: Maybe also test Auth? The old one didn't.
every { network.connectUnauthChat(any()) } answers {
chatListener = firstArg()
delay {
it.complete(chatConnection)
connectLatch?.countDown()
}
}
every { chatConnection.disconnect() } answers {
delay {
it.complete(null)
disconnectLatch?.countDown()
// The disconnectReason is null when the disconnect is due to the local client requesting the disconnect.
// This is a regression test because we previously forgot to update the Kotlin type definitions to
// match this when the behavior changed in libsignal-client, causing NullPointerExceptions
// missed connection interrupted events.
chatListener!!.onConnectionInterrupted(chatConnection, null)
}
}
every { chatConnection.send(any()) } answers {
delay {
it.complete(RESPONSE_SUCCESS)
sendLatch?.countDown()
}
}
every { chatConnection.start() } returns Unit
}
// Test that the LibSignalChatConnection transitions through DISCONNECTED -> CONNECTING -> CONNECTED
// if the underlying ChatConnection future completes successfully.
@Test
fun orderOfStatesOnSuccessfulConnect() {
connectLatch = CountDownLatch(1)
val observer = TestObserver<WebSocketConnectionState>()
connection.state.subscribe(observer)
connection.connect()
connectLatch!!.await(100, TimeUnit.MILLISECONDS)
observer.assertNotComplete()
observer.assertValues(
WebSocketConnectionState.DISCONNECTED,
WebSocketConnectionState.CONNECTING,
WebSocketConnectionState.CONNECTED
)
observer.assertNoConsecutiveDuplicates()
}
// Test that the LibSignalChatConnection transitions to FAILED if the
// underlying ChatConnection future completes exceptionally.
@Test
fun orderOfStatesOnConnectionFailure() {
val connectionException = RuntimeException("connect failed")
val latch = CountDownLatch(1)
every { network.connectUnauthChat(any()) } answers {
chatListener = firstArg()
delay {
it.completeExceptionally(connectionException)
latch.countDown()
}
}
val observer = TestObserver<WebSocketConnectionState>()
connection.state.subscribe(observer)
connection.connect()
latch.await(100, TimeUnit.MILLISECONDS)
observer.assertNotComplete()
observer.assertValues(
WebSocketConnectionState.DISCONNECTED,
WebSocketConnectionState.CONNECTING,
WebSocketConnectionState.FAILED
)
observer.assertNoConsecutiveDuplicates()
}
// Test connect followed by disconnect, checking the state transitions.
@Ignore("Flaky")
@Test
fun orderOfStatesOnConnectAndDisconnect() {
connectLatch = CountDownLatch(1)
disconnectLatch = CountDownLatch(1)
val observer = TestObserver<WebSocketConnectionState>()
connection.state.subscribe(observer)
connection.connect()
connectLatch!!.await(100, TimeUnit.MILLISECONDS)
connection.disconnect()
disconnectLatch!!.await(100, TimeUnit.MILLISECONDS)
// onConnectionInterrupted acts like the onClosed callback for the connection here, driving the
// transition from DISCONNECTING -> DISCONNECTED.
chatListener!!.onConnectionInterrupted(chatConnection, null)
observer.assertNotComplete()
observer.assertValues(
WebSocketConnectionState.DISCONNECTED,
WebSocketConnectionState.CONNECTING,
WebSocketConnectionState.CONNECTED,
WebSocketConnectionState.DISCONNECTING,
WebSocketConnectionState.DISCONNECTED
)
observer.assertNoConsecutiveDuplicates()
}
// Test that a disconnect failure transitions from CONNECTED -> DISCONNECTING -> DISCONNECTED anyway,
// since we don't have a specific "DISCONNECT_FAILED" state.
@Test
fun orderOfStatesOnDisconnectFailure() {
val disconnectException = RuntimeException("disconnect failed")
val disconnectLatch = CountDownLatch(1)
every { chatConnection.disconnect() } answers {
delay {
it.completeExceptionally(disconnectException)
disconnectLatch.countDown()
}
}
setupConnectedConnection()
val observer = TestObserver<WebSocketConnectionState>()
connection.state.subscribe(observer)
connection.disconnect()
disconnectLatch.await(100, TimeUnit.MILLISECONDS)
observer.assertNotComplete()
observer.assertValues(
// The subscriber is created after we've already connected, so the first state it sees is CONNECTED:
WebSocketConnectionState.CONNECTED,
WebSocketConnectionState.DISCONNECTING,
WebSocketConnectionState.DISCONNECTED
)
observer.assertNoConsecutiveDuplicates()
}
// Test a successful keepAlive, i.e. we get a 200 OK in response to the keepAlive request,
// which triggers healthMonitor.onKeepAliveResponse(...) and not onMessageError.
@Test
fun keepAliveSuccess() {
setupConnectedConnection()
sendLatch = CountDownLatch(1)
connection.sendKeepAlive()
sendLatch!!.await(100, TimeUnit.MILLISECONDS)
verify(exactly = 1) {
healthMonitor.onKeepAliveResponse(any(), false)
}
verify(exactly = 0) {
healthMonitor.onMessageError(any(), any())
}
}
// Test keepAlive failures: we get 4xx or 5xx, which triggers healthMonitor.onMessageError(...) but not onKeepAliveResponse.
@Test
fun keepAliveFailure() {
for (response in listOf(RESPONSE_ERROR, RESPONSE_SERVER_ERROR)) {
clearMocks(healthMonitor)
every { chatConnection.send(any()) } answers {
delay {
it.complete(response)
sendLatch?.countDown()
}
}
setupConnectedConnection()
sendLatch = CountDownLatch(1)
connection.sendKeepAlive()
sendLatch!!.await(100, TimeUnit.MILLISECONDS)
verify(exactly = 1) {
healthMonitor.onMessageError(response.status, false)
}
verify(exactly = 0) {
healthMonitor.onKeepAliveResponse(any(), any())
}
}
}
// Test keepAlive that fails at the transport layer (send() throws),
// which transitions from CONNECTED -> DISCONNECTED.
@Test
fun keepAliveConnectionFailure() {
val connectionFailure = RuntimeException("Sending keep-alive failed")
val keepAliveFailureLatch = CountDownLatch(1)
every { chatConnection.send(any()) } answers {
delay {
it.completeExceptionally(connectionFailure)
keepAliveFailureLatch.countDown()
}
}
setupConnectedConnection()
val observer = TestObserver<WebSocketConnectionState>()
connection.state.subscribe(observer)
connection.sendKeepAlive()
keepAliveFailureLatch.await(100, TimeUnit.MILLISECONDS)
observer.assertNotComplete()
observer.assertValues(
// We start in the connected state
WebSocketConnectionState.CONNECTED,
// Disconnects as a result of keep-alive failure
WebSocketConnectionState.DISCONNECTED
)
observer.assertNoConsecutiveDuplicates()
verify(exactly = 0) {
healthMonitor.onKeepAliveResponse(any(), any())
healthMonitor.onMessageError(any(), any())
}
}
// Test that an incoming "connection interrupted" event from ChatConnection sets our state to DISCONNECTED.
@Test
fun connectionInterruptedTest() {
val disconnectReason = ChatServiceException("simulated interrupt")
setupConnectedConnection()
val observer = TestObserver<WebSocketConnectionState>()
connection.state.subscribe(observer)
chatListener!!.onConnectionInterrupted(chatConnection, disconnectReason)
observer.assertNotComplete()
observer.assertValues(
// We start in the connected state
WebSocketConnectionState.CONNECTED,
// Disconnects as a result of the connection interrupted event
WebSocketConnectionState.DISCONNECTED
)
observer.assertNoConsecutiveDuplicates()
verify(exactly = 0) {
healthMonitor.onKeepAliveResponse(any(), any())
healthMonitor.onMessageError(any(), any())
}
}
// If readRequest() does not throw when the underlying connection disconnects, this
// causes the app to get stuck in a "fetching new messages" state.
@Test
fun regressionTestReadRequestThrowsOnDisconnect() {
setupConnectedConnection()
executor.submit {
Thread.sleep(100)
chatConnection.disconnect()
}
assertThrows(IOException::class.java) {
connection.readRequest(1000)
}
}
@Test
fun readRequestDoesTimeOut() {
setupConnectedConnection()
val observer = TestObserver<WebSocketConnectionState>()
connection.state.subscribe(observer)
assertThrows(TimeoutException::class.java) {
connection.readRequest(10)
}
}
// Test reading incoming requests from the queue.
// We'll simulate onIncomingMessage() from the ChatConnectionListener, then read them from the LibSignalChatConnection.
@Test
fun incomingRequests() {
setupConnectedConnection()
val observer = TestObserver<WebSocketConnectionState>()
connection.state.subscribe(observer)
// We'll now simulate incoming messages
val envelopeA = "msgA".toByteArray()
val envelopeB = "msgB".toByteArray()
val envelopeC = "msgC".toByteArray()
val asyncMessageReadLatch = CountDownLatch(1)
// Helper to check that the WebSocketRequestMessage for an envelope is as expected
fun assertRequestWithEnvelope(request: WebSocketRequestMessage, envelope: ByteArray) {
assertEquals("PUT", request.verb)
assertEquals("/api/v1/message", request.path)
assertEquals(envelope.toByteString(), request.body!!)
connection.sendResponse(
WebSocketResponseMessage(
request.id,
200,
"OK"
)
)
}
// Helper to check that a queue-empty request is as expected
fun assertQueueEmptyRequest(request: WebSocketRequestMessage) {
assertEquals("PUT", request.verb)
assertEquals("/api/v1/queue/empty", request.path)
connection.sendResponse(
WebSocketResponseMessage(
request.id,
200,
"OK"
)
)
}
// Read request asynchronously to simulate concurrency
executor.submit {
val request = connection.readRequest(200)
assertRequestWithEnvelope(request, envelopeA)
asyncMessageReadLatch.countDown()
}
chatListener!!.onIncomingMessage(chatConnection, envelopeA, 0, null)
asyncMessageReadLatch.await(100, TimeUnit.MILLISECONDS)
chatListener!!.onIncomingMessage(chatConnection, envelopeB, 0, null)
assertRequestWithEnvelope(connection.readRequestIfAvailable().get(), envelopeB)
chatListener!!.onQueueEmpty(chatConnection)
assertQueueEmptyRequest(connection.readRequestIfAvailable().get())
chatListener!!.onIncomingMessage(chatConnection, envelopeC, 0, null)
assertRequestWithEnvelope(connection.readRequestIfAvailable().get(), envelopeC)
assertTrue(connection.readRequestIfAvailable().isEmpty)
}
@Test
fun regressionTestDisconnectWhileConnecting() {
every { network.connectUnauthChat(any()) } answers {
chatListener = firstArg()
delay {
// We do not complete the future, so we stay in the CONNECTING state forever.
}
}
connection.connect()
connection.disconnect()
}
@Test
fun regressionTestSendWhileConnecting() {
var connectionCompletionFuture: CompletableFuture<UnauthenticatedChatConnection>? = null
every { network.connectUnauthChat(any()) } answers {
chatListener = firstArg()
connectionCompletionFuture = CompletableFuture<UnauthenticatedChatConnection>()
connectionCompletionFuture!!
}
sendLatch = CountDownLatch(1)
connection.connect()
val sendSingle = connection.sendRequest(WebSocketRequestMessage("GET", "/fake-path"))
val sendObserver = sendSingle.test()
assertEquals(1, sendLatch!!.count)
sendObserver.assertNotComplete()
connectionCompletionFuture!!.complete(chatConnection)
sendLatch!!.await(100, TimeUnit.MILLISECONDS)
sendObserver.awaitDone(100, TimeUnit.MILLISECONDS)
sendObserver.assertValues(RESPONSE_SUCCESS.toWebsocketResponse(true))
}
@Test
fun testSendFailsWhenConnectionFails() {
var connectionCompletionFuture: CompletableFuture<UnauthenticatedChatConnection>? = null
every { network.connectUnauthChat(any()) } answers {
chatListener = firstArg()
connectionCompletionFuture = CompletableFuture<UnauthenticatedChatConnection>()
connectionCompletionFuture!!
}
sendLatch = CountDownLatch(1)
connection.connect()
val sendSingle = connection.sendRequest(WebSocketRequestMessage("GET", "/fake-path"))
val sendObserver = sendSingle.test()
assertEquals(1, sendLatch!!.count)
sendObserver.assertNotComplete()
connectionCompletionFuture!!.completeExceptionally(ChatServiceException(""))
sendObserver.awaitDone(100, TimeUnit.MILLISECONDS)
assertEquals(1, sendLatch!!.count)
sendObserver.assertFailure(IOException().javaClass)
}
@Test
fun regressionTestSendAfterConnectionFutureCompletesButBeforeStateUpdates() {
// We used to have a race condition where if sendRequest was called after
// the chatConnectionFuture completed but before the completion handler that
// that updates LibSignalChatConnection's state ran, we would end up with a
// StackOverflowError exception.
// We ended up fixing that bug by refactoring that part of the code completely.
// This tests that scenario to ensure that we don't regress by introducing
// some other kind of bug in that tricky situation.
var connectionFuture: CompletableFuture<UnauthenticatedChatConnection>? = null
val futureCompletedLatch = CountDownLatch(1)
val requestCompletedLatch = CountDownLatch(1)
every { network.connectUnauthChat(any()) } answers {
chatListener = firstArg()
connectionFuture = CompletableFuture<UnauthenticatedChatConnection>()
// Add a completion handler that blocks to prevent state transition
connectionFuture!!.whenComplete { _, _ ->
// When we reach this point, we know connectionFuture.complete
// must have been called, and subsequent calls will return false.
futureCompletedLatch.countDown()
// Block to keep state as CONNECTING
requestCompletedLatch.await()
}
connectionFuture!!
}
connection.connect()
executor.submit {
// This will block until all the completion handlers complete, which
// means it will block until requestCompletedLatch is counted down.
connectionFuture!!.complete(chatConnection)
}
assertTrue("connectionFuture was never completed", futureCompletedLatch.await(100, TimeUnit.MILLISECONDS))
// Now calls to connectionFuture.whenComplete will synchronously
// execute the completionHandler given to them, but the state of
// LibSignalChatConnection will still be CONNECTING.
// Previously, this caused a bug where the completion handler would see
// the state was still CONNECTING, and call connectionFuture.whenComplete
// again, thus setting off an infinite recursive loop, ending in a
// StackOverflowError.
connection.sendRequest(WebSocketRequestMessage("GET", "/test"))
// The test passed! Unblock the executor thread.
requestCompletedLatch.countDown()
}
@Test
fun testQueueLargeNumberOfRequestsWhileConnecting() {
// Test queuing up 100,000 requests while the connection is still CONNECTING,
// then complete the connection to make sure they all send successfully.
var connectionCompletionFuture: CompletableFuture<UnauthenticatedChatConnection>? = null
val sendRequestCount = 100_000
val allSentLatch = CountDownLatch(sendRequestCount)
every { network.connectUnauthChat(any()) } answers {
chatListener = firstArg()
connectionCompletionFuture = CompletableFuture<UnauthenticatedChatConnection>()
connectionCompletionFuture!!
}
every { chatConnection.send(any()) } answers {
delay {
it.complete(RESPONSE_SUCCESS)
allSentLatch.countDown()
}
}
connection.connect()
val sendObservers = mutableListOf<TestObserver<WebsocketResponse>>()
for (i in 0 until sendRequestCount) {
val sendSingle = connection.sendRequest(WebSocketRequestMessage("GET", "/test-path-$i"))
val observer = sendSingle.test()
sendObservers.add(observer)
}
sendObservers.forEach { observer ->
observer.assertNotComplete()
}
connectionCompletionFuture!!.complete(chatConnection)
assertTrue("All $sendRequestCount were not sent", allSentLatch.await(1, TimeUnit.SECONDS))
sendObservers.forEach { observer ->
observer.awaitDone(100, TimeUnit.MILLISECONDS)
observer.assertValues(RESPONSE_SUCCESS.toWebsocketResponse(true))
observer.assertComplete()
}
}
private fun <T> delay(action: ((CompletableFuture<T>) -> Unit)): CompletableFuture<T> {
val future = CompletableFuture<T>()
executor.submit {
action(future)
}
return future
}
private fun TestObserver<WebSocketConnectionState>.assertNoConsecutiveDuplicates() {
val states = this.values()
for (i in 1 until states.size) {
assertNotEquals(
"Found duplicate consecutive states states[${i - 1}] = states[$i] = ${states[i]}",
states[i - 1],
states[i]
)
}
}
companion object {
// For verifying success / error scenarios in keepAlive tests, etc.
private val RESPONSE_SUCCESS = ChatConnection.Response(200, "", emptyMap(), byteArrayOf())
private val RESPONSE_ERROR = ChatConnection.Response(400, "", emptyMap(), byteArrayOf())
private val RESPONSE_SERVER_ERROR = ChatConnection.Response(500, "", emptyMap(), byteArrayOf())
}
}

View File

@@ -0,0 +1,42 @@
package org.whispersystems.signalservice.testutil;
import org.signal.libsignal.internal.Native;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeNoException;
public final class LibSignalLibraryUtil {
/**
* Attempts to initialize the LibSignal Native class, which will load the native binaries.
* <p>
* If that fails to link, then on Unix, it will fail as we rely on that for CI.
* <p>
* If that fails to link, and it's not Unix, it will skip the test via assumption violation.
* <p>
* If using inside a PowerMocked test, the assumption violation can be fatal, use:
* {@code @PowerMockRunnerDelegate(JUnit4.class)}
*/
public static void assumeLibSignalSupportedOnOS() {
try {
Class.forName(Native.class.getName());
} catch (ClassNotFoundException e) {
fail();
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
String osName = System.getProperty("os.name");
if (isUnix(osName)) {
fail("Not able to link native LibSignal on a key OS: " + osName);
} else {
assumeNoException("Not able to link native LibSignal on this operating system: " + osName, e);
}
}
}
private static boolean isUnix(String osName) {
assertNotNull(osName);
osName = osName.toLowerCase();
return osName.contains("nix") || osName.contains("nux") || osName.contains("aix");
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.