Let server generate copyToMedia IVs
We include the IV in the encrypted payload, so we can let the server choose them instead of the client
This commit is contained in:
parent
a5f60b1522
commit
f2cb04817b
|
@ -19,7 +19,6 @@ import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionStage;
|
import java.util.concurrent.CompletionStage;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
|
@ -129,7 +128,7 @@ public class Cdn3RemoteStorageManager implements RemoteStorageManager {
|
||||||
* Serialized copy request for cdn3 storage manager
|
* Serialized copy request for cdn3 storage manager
|
||||||
*/
|
*/
|
||||||
record Cdn3CopyRequest(
|
record Cdn3CopyRequest(
|
||||||
String encryptionKey, String hmacKey, String iv,
|
String encryptionKey, String hmacKey,
|
||||||
SourceDescriptor source, int expectedSourceLength,
|
SourceDescriptor source, int expectedSourceLength,
|
||||||
String dst) {
|
String dst) {
|
||||||
|
|
||||||
|
@ -137,7 +136,6 @@ public class Cdn3RemoteStorageManager implements RemoteStorageManager {
|
||||||
String dst) {
|
String dst) {
|
||||||
this(Base64.getEncoder().encodeToString(parameters.aesEncryptionKey().getEncoded()),
|
this(Base64.getEncoder().encodeToString(parameters.aesEncryptionKey().getEncoded()),
|
||||||
Base64.getEncoder().encodeToString(parameters.hmacSHA256Key().getEncoded()),
|
Base64.getEncoder().encodeToString(parameters.hmacSHA256Key().getEncoded()),
|
||||||
Base64.getEncoder().encodeToString(parameters.iv().getIV()),
|
|
||||||
source, expectedSourceLength, dst);
|
source, expectedSourceLength, dst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
package org.whispersystems.textsecuregcm.backup;
|
package org.whispersystems.textsecuregcm.backup;
|
||||||
|
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
public record MediaEncryptionParameters(
|
public record MediaEncryptionParameters(
|
||||||
SecretKeySpec aesEncryptionKey,
|
SecretKeySpec aesEncryptionKey,
|
||||||
SecretKeySpec hmacSHA256Key,
|
SecretKeySpec hmacSHA256Key) {
|
||||||
IvParameterSpec iv) {
|
|
||||||
|
|
||||||
public MediaEncryptionParameters(byte[] encryptionKey, byte[] macKey, byte[] iv) {
|
public MediaEncryptionParameters(byte[] encryptionKey, byte[] macKey) {
|
||||||
this(
|
this(
|
||||||
new SecretKeySpec(encryptionKey, "AES"),
|
new SecretKeySpec(encryptionKey, "AES"),
|
||||||
new SecretKeySpec(macKey, "HmacSHA256"),
|
new SecretKeySpec(macKey, "HmacSHA256"));
|
||||||
new IvParameterSpec(iv));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int outputSize(final int inputSize) {
|
public int outputSize(final int inputSize) {
|
||||||
// AES-256 has 16-byte block size, and always adds a block if the plaintext is a multiple of the block size
|
// AES-256 has 16-byte block size, and always adds a block if the plaintext is a multiple of the block size
|
||||||
final int numBlocks = (inputSize + 16) / 16;
|
final int numBlocks = (inputSize + 16) / 16;
|
||||||
|
// 16-byte IV will be generated and prepended to the ciphertext
|
||||||
// IV + AES-256 encrypted data + HmacSHA256
|
// IV + AES-256 encrypted data + HmacSHA256
|
||||||
return this.iv().getIV().length + (numBlocks * 16) + 32;
|
return 16 + (numBlocks * 16) + 32;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -512,19 +512,13 @@ public class ArchiveController {
|
||||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||||
@NotNull
|
@NotNull
|
||||||
@ExactlySize(32)
|
@ExactlySize(32)
|
||||||
byte[] encryptionKey,
|
byte[] encryptionKey) {
|
||||||
|
|
||||||
@Schema(description = "A 16-byte IV for AES, encoded in standard padded base64", implementation = String.class)
|
|
||||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
|
||||||
@NotNull
|
|
||||||
@ExactlySize(16)
|
|
||||||
byte[] iv) {
|
|
||||||
|
|
||||||
CopyParameters toCopyParameters() {
|
CopyParameters toCopyParameters() {
|
||||||
return new CopyParameters(
|
return new CopyParameters(
|
||||||
sourceAttachment.cdn(), sourceAttachment.key(),
|
sourceAttachment.cdn(), sourceAttachment.key(),
|
||||||
objectLength,
|
objectLength,
|
||||||
new MediaEncryptionParameters(encryptionKey, hmacKey, iv),
|
new MediaEncryptionParameters(encryptionKey, hmacKey),
|
||||||
mediaId);
|
mediaId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,8 +85,7 @@ public class BackupManagerTest {
|
||||||
|
|
||||||
private static final MediaEncryptionParameters COPY_ENCRYPTION_PARAM = new MediaEncryptionParameters(
|
private static final MediaEncryptionParameters COPY_ENCRYPTION_PARAM = new MediaEncryptionParameters(
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32),
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32));
|
||||||
TestRandomUtil.nextBytes(16));
|
|
||||||
private static final CopyParameters COPY_PARAM = new CopyParameters(
|
private static final CopyParameters COPY_PARAM = new CopyParameters(
|
||||||
3, "abc", 100,
|
3, "abc", 100,
|
||||||
COPY_ENCRYPTION_PARAM, TestRandomUtil.nextBytes(15));
|
COPY_ENCRYPTION_PARAM, TestRandomUtil.nextBytes(15));
|
||||||
|
|
|
@ -39,7 +39,6 @@ public class Cdn3RemoteStorageManagerTest {
|
||||||
|
|
||||||
private static final byte[] HMAC_KEY = TestRandomUtil.nextBytes(32);
|
private static final byte[] HMAC_KEY = TestRandomUtil.nextBytes(32);
|
||||||
private static final byte[] AES_KEY = TestRandomUtil.nextBytes(32);
|
private static final byte[] AES_KEY = TestRandomUtil.nextBytes(32);
|
||||||
private static final byte[] IV = TestRandomUtil.nextBytes(16);
|
|
||||||
|
|
||||||
@RegisterExtension
|
@RegisterExtension
|
||||||
private static final WireMockExtension wireMock = WireMockExtension.newInstance()
|
private static final WireMockExtension wireMock = WireMockExtension.newInstance()
|
||||||
|
@ -66,7 +65,7 @@ public class Cdn3RemoteStorageManagerTest {
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(ints = {2, 3})
|
@ValueSource(ints = {2, 3})
|
||||||
public void copy(final int sourceCdn) throws JsonProcessingException {
|
public void copy(final int sourceCdn) throws JsonProcessingException {
|
||||||
final MediaEncryptionParameters encryptionParameters = new MediaEncryptionParameters(AES_KEY, HMAC_KEY, IV);
|
final MediaEncryptionParameters encryptionParameters = new MediaEncryptionParameters(AES_KEY, HMAC_KEY);
|
||||||
final String scheme = switch (sourceCdn) {
|
final String scheme = switch (sourceCdn) {
|
||||||
case 2 -> "gcs";
|
case 2 -> "gcs";
|
||||||
case 3 -> "r2";
|
case 3 -> "r2";
|
||||||
|
@ -99,7 +98,7 @@ public class Cdn3RemoteStorageManagerTest {
|
||||||
2,
|
2,
|
||||||
"a/test/source",
|
"a/test/source",
|
||||||
100,
|
100,
|
||||||
new MediaEncryptionParameters(AES_KEY, HMAC_KEY, IV),
|
new MediaEncryptionParameters(AES_KEY, HMAC_KEY),
|
||||||
"a/destination").toCompletableFuture());
|
"a/destination").toCompletableFuture());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +110,7 @@ public class Cdn3RemoteStorageManagerTest {
|
||||||
2,
|
2,
|
||||||
"a/test/source",
|
"a/test/source",
|
||||||
100,
|
100,
|
||||||
new MediaEncryptionParameters(AES_KEY, HMAC_KEY, IV),
|
new MediaEncryptionParameters(AES_KEY, HMAC_KEY),
|
||||||
"a/destination").toCompletableFuture());
|
"a/destination").toCompletableFuture());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +121,7 @@ public class Cdn3RemoteStorageManagerTest {
|
||||||
0,
|
0,
|
||||||
"a/test/source",
|
"a/test/source",
|
||||||
100,
|
100,
|
||||||
new MediaEncryptionParameters(AES_KEY, HMAC_KEY, IV),
|
new MediaEncryptionParameters(AES_KEY, HMAC_KEY),
|
||||||
"a/destination").toCompletableFuture());
|
"a/destination").toCompletableFuture());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -272,8 +272,7 @@ public class ArchiveControllerTest {
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource
|
@MethodSource
|
||||||
public void setBackupIdException(final Exception ex, final boolean sync, final int expectedStatus)
|
public void setBackupIdException(final Exception ex, final boolean sync, final int expectedStatus) {
|
||||||
throws RateLimitExceededException {
|
|
||||||
if (sync) {
|
if (sync) {
|
||||||
when(backupAuthManager.commitBackupId(any(), any(), any())).thenThrow(ex);
|
when(backupAuthManager.commitBackupId(any(), any(), any())).thenThrow(ex);
|
||||||
} else {
|
} else {
|
||||||
|
@ -393,16 +392,14 @@ public class ArchiveControllerTest {
|
||||||
100,
|
100,
|
||||||
mediaIds[0],
|
mediaIds[0],
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32),
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32)),
|
||||||
TestRandomUtil.nextBytes(16)),
|
|
||||||
|
|
||||||
new ArchiveController.CopyMediaRequest(
|
new ArchiveController.CopyMediaRequest(
|
||||||
new RemoteAttachment(3, "def"),
|
new RemoteAttachment(3, "def"),
|
||||||
200,
|
200,
|
||||||
mediaIds[1],
|
mediaIds[1],
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32),
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32))
|
||||||
TestRandomUtil.nextBytes(16))
|
|
||||||
))));
|
))));
|
||||||
assertThat(r.getStatus()).isEqualTo(207);
|
assertThat(r.getStatus()).isEqualTo(207);
|
||||||
final ArchiveController.CopyMediaBatchResponse copyResponse = r.readEntity(
|
final ArchiveController.CopyMediaBatchResponse copyResponse = r.readEntity(
|
||||||
|
@ -438,8 +435,7 @@ public class ArchiveControllerTest {
|
||||||
100,
|
100,
|
||||||
mediaId,
|
mediaId,
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32),
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32))
|
||||||
TestRandomUtil.nextBytes(16))
|
|
||||||
).toList();
|
).toList();
|
||||||
|
|
||||||
Response r = resources.getJerseyTest()
|
Response r = resources.getJerseyTest()
|
||||||
|
@ -454,7 +450,7 @@ public class ArchiveControllerTest {
|
||||||
|
|
||||||
assertThat(copyResponse.responses()).hasSize(4);
|
assertThat(copyResponse.responses()).hasSize(4);
|
||||||
|
|
||||||
final ArchiveController.CopyMediaBatchResponse.Entry r1 = copyResponse.responses().get(0);
|
final ArchiveController.CopyMediaBatchResponse.Entry r1 = copyResponse.responses().getFirst();
|
||||||
assertThat(r1.cdn()).isEqualTo(1);
|
assertThat(r1.cdn()).isEqualTo(1);
|
||||||
assertThat(r1.mediaId()).isEqualTo(mediaIds[0]);
|
assertThat(r1.mediaId()).isEqualTo(mediaIds[0]);
|
||||||
assertThat(r1.status()).isEqualTo(200);
|
assertThat(r1.status()).isEqualTo(200);
|
||||||
|
@ -494,16 +490,14 @@ public class ArchiveControllerTest {
|
||||||
1,
|
1,
|
||||||
mediaIds[0],
|
mediaIds[0],
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32),
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32)),
|
||||||
TestRandomUtil.nextBytes(16)),
|
|
||||||
|
|
||||||
new ArchiveController.CopyMediaRequest(
|
new ArchiveController.CopyMediaRequest(
|
||||||
new RemoteAttachment(3, "def"),
|
new RemoteAttachment(3, "def"),
|
||||||
-1,
|
-1,
|
||||||
mediaIds[1],
|
mediaIds[1],
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32),
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32))
|
||||||
TestRandomUtil.nextBytes(16))
|
|
||||||
))));
|
))));
|
||||||
assertThat(r.getStatus()).isEqualTo(422);
|
assertThat(r.getStatus()).isEqualTo(422);
|
||||||
}
|
}
|
||||||
|
@ -573,7 +567,7 @@ public class ArchiveControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void mediaUploadForm() throws RateLimitExceededException, VerificationFailedException {
|
public void mediaUploadForm() throws VerificationFailedException {
|
||||||
final BackupAuthCredentialPresentation presentation =
|
final BackupAuthCredentialPresentation presentation =
|
||||||
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
|
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
|
||||||
when(backupManager.authenticateBackupUser(any(), any()))
|
when(backupManager.authenticateBackupUser(any(), any()))
|
||||||
|
@ -675,8 +669,7 @@ public class ArchiveControllerTest {
|
||||||
100,
|
100,
|
||||||
TestRandomUtil.nextBytes(15),
|
TestRandomUtil.nextBytes(15),
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32),
|
||||||
TestRandomUtil.nextBytes(32),
|
TestRandomUtil.nextBytes(32))));
|
||||||
TestRandomUtil.nextBytes(16))));
|
|
||||||
assertThat(r.getStatus()).isEqualTo(422);
|
assertThat(r.getStatus()).isEqualTo(422);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue