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:
Ravi Khadiwala 2024-10-28 17:28:23 -05:00 committed by Jon Chambers
parent a5f60b1522
commit f2cb04817b
6 changed files with 22 additions and 41 deletions

View File

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

View File

@ -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;
} }
} }

View File

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

View File

@ -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));

View File

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

View File

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