Commit d93b57c0 by olly Committed by Oliver Woodman

Zero out trailing bytes in CryptoInfo.iv

CryptoInfo.iv length is always 16. When the actual initialization vector
is shorter, zero out the trailing bytes.

Issue: #6982
PiperOrigin-RevId: 295575845
parent a967ff32
# Release notes # # Release notes #
### 2.11.3 (2020-02-19) ###
* DRM: Fix issue switching from protected content that uses a 16-byte
initialization vector to one that uses an 8-byte initialization vector
([#6982](https://github.com/google/ExoPlayer/issues/6982)).
### 2.11.2 (2020-02-13) ### ### 2.11.2 (2020-02-13) ###
* Add Java FLAC extractor * Add Java FLAC extractor
......
...@@ -25,27 +25,41 @@ import com.google.android.exoplayer2.util.Util; ...@@ -25,27 +25,41 @@ import com.google.android.exoplayer2.util.Util;
public final class CryptoInfo { public final class CryptoInfo {
/** /**
* The 16 byte initialization vector. If the initialization vector of the content is shorter than
* 16 bytes, 0 byte padding is appended to extend the vector to the required 16 byte length.
*
* @see android.media.MediaCodec.CryptoInfo#iv * @see android.media.MediaCodec.CryptoInfo#iv
*/ */
public byte[] iv; public byte[] iv;
/** /**
* The 16 byte key id.
*
* @see android.media.MediaCodec.CryptoInfo#key * @see android.media.MediaCodec.CryptoInfo#key
*/ */
public byte[] key; public byte[] key;
/** /**
* The type of encryption that has been applied. Must be one of the {@link C.CryptoMode} values.
*
* @see android.media.MediaCodec.CryptoInfo#mode * @see android.media.MediaCodec.CryptoInfo#mode
*/ */
@C.CryptoMode @C.CryptoMode public int mode;
public int mode;
/** /**
* The number of leading unencrypted bytes in each sub-sample. If null, all bytes are treated as
* encrypted and {@link #numBytesOfEncryptedData} must be specified.
*
* @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData * @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData
*/ */
public int[] numBytesOfClearData; public int[] numBytesOfClearData;
/** /**
* The number of trailing encrypted bytes in each sub-sample. If null, all bytes are treated as
* clear and {@link #numBytesOfClearData} must be specified.
*
* @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData * @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData
*/ */
public int[] numBytesOfEncryptedData; public int[] numBytesOfEncryptedData;
/** /**
* The number of subSamples that make up the buffer's contents.
*
* @see android.media.MediaCodec.CryptoInfo#numSubSamples * @see android.media.MediaCodec.CryptoInfo#numSubSamples
*/ */
public int numSubSamples; public int numSubSamples;
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData;
...@@ -27,6 +28,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -27,6 +28,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays;
/** A queue of media sample data. */ /** A queue of media sample data. */
/* package */ class SampleDataQueue { /* package */ class SampleDataQueue {
...@@ -228,10 +230,14 @@ import java.nio.ByteBuffer; ...@@ -228,10 +230,14 @@ import java.nio.ByteBuffer;
int ivSize = signalByte & 0x7F; int ivSize = signalByte & 0x7F;
// Read the initialization vector. // Read the initialization vector.
if (buffer.cryptoInfo.iv == null) { CryptoInfo cryptoInfo = buffer.cryptoInfo;
buffer.cryptoInfo.iv = new byte[16]; if (cryptoInfo.iv == null) {
cryptoInfo.iv = new byte[16];
} else {
// Zero out cryptoInfo.iv so that if ivSize < 16, the remaining bytes are correctly set to 0.
Arrays.fill(cryptoInfo.iv, (byte) 0);
} }
readData(offset, buffer.cryptoInfo.iv, ivSize); readData(offset, cryptoInfo.iv, ivSize);
offset += ivSize; offset += ivSize;
// Read the subsample count, if present. // Read the subsample count, if present.
...@@ -246,11 +252,11 @@ import java.nio.ByteBuffer; ...@@ -246,11 +252,11 @@ import java.nio.ByteBuffer;
} }
// Write the clear and encrypted subsample sizes. // Write the clear and encrypted subsample sizes.
int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData; @Nullable int[] clearDataSizes = cryptoInfo.numBytesOfClearData;
if (clearDataSizes == null || clearDataSizes.length < subsampleCount) { if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
clearDataSizes = new int[subsampleCount]; clearDataSizes = new int[subsampleCount];
} }
int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData; @Nullable int[] encryptedDataSizes = cryptoInfo.numBytesOfEncryptedData;
if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) { if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
encryptedDataSizes = new int[subsampleCount]; encryptedDataSizes = new int[subsampleCount];
} }
...@@ -271,12 +277,12 @@ import java.nio.ByteBuffer; ...@@ -271,12 +277,12 @@ import java.nio.ByteBuffer;
// Populate the cryptoInfo. // Populate the cryptoInfo.
CryptoData cryptoData = extrasHolder.cryptoData; CryptoData cryptoData = extrasHolder.cryptoData;
buffer.cryptoInfo.set( cryptoInfo.set(
subsampleCount, subsampleCount,
clearDataSizes, clearDataSizes,
encryptedDataSizes, encryptedDataSizes,
cryptoData.encryptionKey, cryptoData.encryptionKey,
buffer.cryptoInfo.iv, cryptoInfo.iv,
cryptoData.cryptoMode, cryptoData.cryptoMode,
cryptoData.encryptedBlocks, cryptoData.encryptedBlocks,
cryptoData.clearBlocks); cryptoData.clearBlocks);
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.source; package com.google.android.exoplayer2.source;
import static com.google.android.exoplayer2.C.BUFFER_FLAG_ENCRYPTED;
import static com.google.android.exoplayer2.C.BUFFER_FLAG_KEY_FRAME; import static com.google.android.exoplayer2.C.BUFFER_FLAG_KEY_FRAME;
import static com.google.android.exoplayer2.C.RESULT_BUFFER_READ; import static com.google.android.exoplayer2.C.RESULT_BUFFER_READ;
import static com.google.android.exoplayer2.C.RESULT_FORMAT_READ; import static com.google.android.exoplayer2.C.RESULT_FORMAT_READ;
...@@ -22,6 +23,7 @@ import static com.google.android.exoplayer2.C.RESULT_NOTHING_READ; ...@@ -22,6 +23,7 @@ import static com.google.android.exoplayer2.C.RESULT_NOTHING_READ;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.lang.Long.MIN_VALUE; import static java.lang.Long.MIN_VALUE;
import static java.util.Arrays.copyOfRange; import static java.util.Arrays.copyOfRange;
import static org.junit.Assert.assertArrayEquals;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
...@@ -114,17 +116,13 @@ public final class SampleQueueTest { ...@@ -114,17 +116,13 @@ public final class SampleQueueTest {
C.BUFFER_FLAG_KEY_FRAME, C.BUFFER_FLAG_ENCRYPTED, 0, C.BUFFER_FLAG_ENCRYPTED, C.BUFFER_FLAG_KEY_FRAME, C.BUFFER_FLAG_ENCRYPTED, 0, C.BUFFER_FLAG_ENCRYPTED,
}; };
private static final long[] ENCRYPTED_SAMPLE_TIMESTAMPS = new long[] {0, 1000, 2000, 3000}; private static final long[] ENCRYPTED_SAMPLE_TIMESTAMPS = new long[] {0, 1000, 2000, 3000};
private static final Format[] ENCRYPTED_SAMPLES_FORMATS = private static final Format[] ENCRYPTED_SAMPLE_FORMATS =
new Format[] {FORMAT_ENCRYPTED, FORMAT_ENCRYPTED, FORMAT_1, FORMAT_ENCRYPTED}; new Format[] {FORMAT_ENCRYPTED, FORMAT_ENCRYPTED, FORMAT_1, FORMAT_ENCRYPTED};
/** Encrypted samples require the encryption preamble. */ /** Encrypted samples require the encryption preamble. */
private static final int[] ENCRYPTED_SAMPLES_SIZES = new int[] {1, 3, 1, 3}; private static final int[] ENCRYPTED_SAMPLE_SIZES = new int[] {1, 3, 1, 3};
private static final int[] ENCRYPTED_SAMPLES_OFFSETS = new int[] {7, 4, 3, 0}; private static final int[] ENCRYPTED_SAMPLE_OFFSETS = new int[] {7, 4, 3, 0};
private static final byte[] ENCRYPTED_SAMPLES_DATA = new byte[8]; private static final byte[] ENCRYPTED_SAMPLE_DATA = new byte[] {1, 1, 1, 1, 1, 1, 1, 1};
static {
Arrays.fill(ENCRYPTED_SAMPLES_DATA, (byte) 1);
}
private static final TrackOutput.CryptoData DUMMY_CRYPTO_DATA = private static final TrackOutput.CryptoData DUMMY_CRYPTO_DATA =
new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, new byte[16], 0, 0); new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, new byte[16], 0, 0);
...@@ -461,6 +459,60 @@ public final class SampleQueueTest { ...@@ -461,6 +459,60 @@ public final class SampleQueueTest {
/* decodeOnlyUntilUs= */ 0); /* decodeOnlyUntilUs= */ 0);
assertThat(result).isEqualTo(RESULT_FORMAT_READ); assertThat(result).isEqualTo(RESULT_FORMAT_READ);
assertThat(formatHolder.drmSession).isSameInstanceAs(mockDrmSession); assertThat(formatHolder.drmSession).isSameInstanceAs(mockDrmSession);
assertReadEncryptedSample(/* sampleIndex= */ 3);
}
@Test
@SuppressWarnings("unchecked")
public void testTrailingCryptoInfoInitializationVectorBytesZeroed() {
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
DrmSession<ExoMediaCrypto> mockPlaceholderDrmSession =
(DrmSession<ExoMediaCrypto>) Mockito.mock(DrmSession.class);
when(mockPlaceholderDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
when(mockDrmSessionManager.acquirePlaceholderSession(
ArgumentMatchers.any(), ArgumentMatchers.anyInt()))
.thenReturn(mockPlaceholderDrmSession);
writeFormat(ENCRYPTED_SAMPLE_FORMATS[0]);
byte[] sampleData = new byte[] {0, 1, 2};
byte[] initializationVector = new byte[] {7, 6, 5, 4, 3, 2, 1, 0};
byte[] encryptedSampleData =
TestUtil.joinByteArrays(
new byte[] {
0x08, // subsampleEncryption = false (1 bit), ivSize = 8 (7 bits).
},
initializationVector,
sampleData);
writeSample(
encryptedSampleData, /* timestampUs= */ 0, BUFFER_FLAG_KEY_FRAME | BUFFER_FLAG_ENCRYPTED);
int result =
sampleQueue.read(
formatHolder,
inputBuffer,
/* formatRequired= */ false,
/* loadingFinished= */ false,
/* decodeOnlyUntilUs= */ 0);
assertThat(result).isEqualTo(RESULT_FORMAT_READ);
// Fill cryptoInfo.iv with non-zero data. When the 8 byte initialization vector is written into
// it, we expect the trailing 8 bytes to be zeroed.
inputBuffer.cryptoInfo.iv = new byte[16];
Arrays.fill(inputBuffer.cryptoInfo.iv, (byte) 1);
result =
sampleQueue.read(
formatHolder,
inputBuffer,
/* formatRequired= */ false,
/* loadingFinished= */ false,
/* decodeOnlyUntilUs= */ 0);
assertThat(result).isEqualTo(RESULT_BUFFER_READ);
// Assert cryptoInfo.iv contains the 8-byte initialization vector and that the trailing 8 bytes
// have been zeroed.
byte[] expectedInitializationVector = Arrays.copyOf(initializationVector, 16);
assertArrayEquals(expectedInitializationVector, inputBuffer.cryptoInfo.iv);
} }
@Test @Test
...@@ -995,11 +1047,11 @@ public final class SampleQueueTest { ...@@ -995,11 +1047,11 @@ public final class SampleQueueTest {
private void writeTestDataWithEncryptedSections() { private void writeTestDataWithEncryptedSections() {
writeTestData( writeTestData(
ENCRYPTED_SAMPLES_DATA, ENCRYPTED_SAMPLE_DATA,
ENCRYPTED_SAMPLES_SIZES, ENCRYPTED_SAMPLE_SIZES,
ENCRYPTED_SAMPLES_OFFSETS, ENCRYPTED_SAMPLE_OFFSETS,
ENCRYPTED_SAMPLE_TIMESTAMPS, ENCRYPTED_SAMPLE_TIMESTAMPS,
ENCRYPTED_SAMPLES_FORMATS, ENCRYPTED_SAMPLE_FORMATS,
ENCRYPTED_SAMPLES_FLAGS); ENCRYPTED_SAMPLES_FLAGS);
} }
...@@ -1033,7 +1085,12 @@ public final class SampleQueueTest { ...@@ -1033,7 +1085,12 @@ public final class SampleQueueTest {
/** Writes a single sample to {@code sampleQueue}. */ /** Writes a single sample to {@code sampleQueue}. */
private void writeSample(byte[] data, long timestampUs, int sampleFlags) { private void writeSample(byte[] data, long timestampUs, int sampleFlags) {
sampleQueue.sampleData(new ParsableByteArray(data), data.length); sampleQueue.sampleData(new ParsableByteArray(data), data.length);
sampleQueue.sampleMetadata(timestampUs, sampleFlags, data.length, 0, null); sampleQueue.sampleMetadata(
timestampUs,
sampleFlags,
data.length,
/* offset= */ 0,
(sampleFlags & C.BUFFER_FLAG_ENCRYPTED) != 0 ? DUMMY_CRYPTO_DATA : null);
} }
/** /**
...@@ -1206,7 +1263,7 @@ public final class SampleQueueTest { ...@@ -1206,7 +1263,7 @@ public final class SampleQueueTest {
} }
private void assertReadEncryptedSample(int sampleIndex) { private void assertReadEncryptedSample(int sampleIndex) {
byte[] sampleData = new byte[ENCRYPTED_SAMPLES_SIZES[sampleIndex]]; byte[] sampleData = new byte[ENCRYPTED_SAMPLE_SIZES[sampleIndex]];
Arrays.fill(sampleData, (byte) 1); Arrays.fill(sampleData, (byte) 1);
boolean isKeyFrame = (ENCRYPTED_SAMPLES_FLAGS[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0; boolean isKeyFrame = (ENCRYPTED_SAMPLES_FLAGS[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0;
boolean isEncrypted = (ENCRYPTED_SAMPLES_FLAGS[sampleIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0; boolean isEncrypted = (ENCRYPTED_SAMPLES_FLAGS[sampleIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0;
...@@ -1216,7 +1273,7 @@ public final class SampleQueueTest { ...@@ -1216,7 +1273,7 @@ public final class SampleQueueTest {
isEncrypted, isEncrypted,
sampleData, sampleData,
/* offset= */ 0, /* offset= */ 0,
ENCRYPTED_SAMPLES_SIZES[sampleIndex] - (isEncrypted ? 2 : 0)); ENCRYPTED_SAMPLE_SIZES[sampleIndex] - (isEncrypted ? 2 : 0));
} }
/** /**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment