Commit d1e4a63a by olly Committed by Ian Baker

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 27bd1294
......@@ -71,6 +71,12 @@
show how to render video to a `GLSurfaceView` while applying a GL shader.
([#6920](https://github.com/google/ExoPlayer/issues/6920)).
### 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) ###
* Add Java FLAC extractor
......
......@@ -27,27 +27,41 @@ import java.util.Arrays;
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
*/
public byte[] iv;
/**
* The 16 byte key id.
*
* @see android.media.MediaCodec.CryptoInfo#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
*/
@C.CryptoMode
public int mode;
@C.CryptoMode 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
*/
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
*/
public int[] numBytesOfEncryptedData;
/**
* The number of subSamples that make up the buffer's contents.
*
* @see android.media.MediaCodec.CryptoInfo#numSubSamples
*/
public int numSubSamples;
......@@ -112,10 +126,10 @@ public final class CryptoInfo {
// Update cryptoInfo fields directly because CryptoInfo.set performs an unnecessary
// object allocation on Android N.
cryptoInfo.numSubSamples = numSubSamples;
cryptoInfo.numBytesOfClearData = copyOrNull(frameworkCryptoInfo.numBytesOfClearData);
cryptoInfo.numBytesOfEncryptedData = copyOrNull(frameworkCryptoInfo.numBytesOfEncryptedData);
cryptoInfo.key = copyOrNull(frameworkCryptoInfo.key);
cryptoInfo.iv = copyOrNull(frameworkCryptoInfo.iv);
cryptoInfo.numBytesOfClearData = copyOrNull(numBytesOfClearData);
cryptoInfo.numBytesOfEncryptedData = copyOrNull(numBytesOfEncryptedData);
cryptoInfo.key = copyOrNull(key);
cryptoInfo.iv = copyOrNull(iv);
cryptoInfo.mode = mode;
if (Util.SDK_INT >= 24) {
android.media.MediaCodec.CryptoInfo.Pattern pattern = patternHolder.pattern;
......@@ -148,31 +162,19 @@ public final class CryptoInfo {
if (count == 0) {
return;
}
if (numBytesOfClearData == null) {
numBytesOfClearData = new int[1];
}
numBytesOfClearData[0] += count;
// It is OK to have numBytesOfClearData and frameworkCryptoInfo.numBytesOfClearData point to
// the same array, see set().
if (frameworkCryptoInfo.numBytesOfClearData == null) {
frameworkCryptoInfo.numBytesOfClearData = numBytesOfClearData;
}
// Update frameworkCryptoInfo.numBytesOfClearData only if it points to a different array than
// numBytesOfClearData (all fields are public and non-final, therefore they can set be set
// directly without calling set()). Otherwise, the array has been updated already in the steps
// above.
if (frameworkCryptoInfo.numBytesOfClearData != numBytesOfClearData) {
frameworkCryptoInfo.numBytesOfClearData[0] += count;
}
numBytesOfClearData[0] += count;
}
@Nullable
private static int[] copyOrNull(@Nullable int[] array) {
return array != null ? Arrays.copyOf(array, array.length) : null;
}
@Nullable
private static byte[] copyOrNull(@Nullable byte[] array) {
return array != null ? Arrays.copyOf(array, array.length) : null;
}
......
......@@ -65,35 +65,4 @@ public class CryptoInfoTest {
assertThat(cryptoInfo.numBytesOfClearData[0]).isEqualTo(6);
assertThat(cryptoInfo.getFrameworkCryptoInfo().numBytesOfClearData[0]).isEqualTo(6);
}
@Test
public void increaseClearDataFirstSubSampleBy_withDifferentClearDataArrays_setsValue() {
cryptoInfo.numBytesOfClearData = new int[] {1, 1, 1, 1};
cryptoInfo.getFrameworkCryptoInfo().numBytesOfClearData = new int[] {5, 5, 5, 5};
cryptoInfo.increaseClearDataFirstSubSampleBy(5);
assertThat(cryptoInfo.numBytesOfClearData[0]).isEqualTo(6);
assertThat(cryptoInfo.getFrameworkCryptoInfo().numBytesOfClearData[0]).isEqualTo(10);
}
@Test
public void increaseClearDataFirstSubSampleBy_withInternalClearDataArraysNull_setsValue() {
cryptoInfo.numBytesOfClearData = new int[] {10, 10, 10, 10};
cryptoInfo.increaseClearDataFirstSubSampleBy(5);
assertThat(cryptoInfo.numBytesOfClearData[0]).isEqualTo(15);
assertThat(cryptoInfo.getFrameworkCryptoInfo().numBytesOfClearData[0]).isEqualTo(15);
}
@Test
public void increaseClearDataFirstSubSampleBy_internalClearDataIsNotNull_setsValue() {
cryptoInfo.getFrameworkCryptoInfo().numBytesOfClearData = new int[] {5, 5, 5, 5};
cryptoInfo.increaseClearDataFirstSubSampleBy(5);
assertThat(cryptoInfo.numBytesOfClearData[0]).isEqualTo(5);
assertThat(cryptoInfo.getFrameworkCryptoInfo().numBytesOfClearData[0]).isEqualTo(10);
}
}
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import androidx.annotation.Nullable;
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.extractor.SampleDataReader;
import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData;
......@@ -28,6 +29,7 @@ import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
/** A queue of media sample data. */
/* package */ class SampleDataQueue {
......@@ -229,10 +231,14 @@ import java.nio.ByteBuffer;
int ivSize = signalByte & 0x7F;
// Read the initialization vector.
if (buffer.cryptoInfo.iv == null) {
buffer.cryptoInfo.iv = new byte[16];
CryptoInfo cryptoInfo = buffer.cryptoInfo;
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;
// Read the subsample count, if present.
......@@ -247,11 +253,11 @@ import java.nio.ByteBuffer;
}
// Write the clear and encrypted subsample sizes.
@Nullable int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData;
@Nullable int[] clearDataSizes = cryptoInfo.numBytesOfClearData;
if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
clearDataSizes = new int[subsampleCount];
}
@Nullable int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData;
@Nullable int[] encryptedDataSizes = cryptoInfo.numBytesOfEncryptedData;
if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
encryptedDataSizes = new int[subsampleCount];
}
......@@ -272,12 +278,12 @@ import java.nio.ByteBuffer;
// Populate the cryptoInfo.
CryptoData cryptoData = Util.castNonNull(extrasHolder.cryptoData);
buffer.cryptoInfo.set(
cryptoInfo.set(
subsampleCount,
clearDataSizes,
encryptedDataSizes,
cryptoData.encryptionKey,
buffer.cryptoInfo.iv,
cryptoInfo.iv,
cryptoData.cryptoMode,
cryptoData.encryptedBlocks,
cryptoData.clearBlocks);
......
......@@ -15,6 +15,7 @@
*/
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.RESULT_BUFFER_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;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.Long.MIN_VALUE;
import static java.util.Arrays.copyOfRange;
import static org.junit.Assert.assertArrayEquals;
import static org.mockito.Mockito.when;
import androidx.annotation.Nullable;
......@@ -114,17 +116,13 @@ public final class SampleQueueTest {
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 Format[] ENCRYPTED_SAMPLES_FORMATS =
private static final Format[] ENCRYPTED_SAMPLE_FORMATS =
new Format[] {FORMAT_ENCRYPTED, FORMAT_ENCRYPTED, FORMAT_1, FORMAT_ENCRYPTED};
/** 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 byte[] ENCRYPTED_SAMPLES_DATA = new byte[8];
static {
Arrays.fill(ENCRYPTED_SAMPLES_DATA, (byte) 1);
}
private static final int[] ENCRYPTED_SAMPLE_OFFSETS = new int[] {7, 4, 3, 0};
private static final byte[] ENCRYPTED_SAMPLE_DATA = new byte[] {1, 1, 1, 1, 1, 1, 1, 1};
private static final TrackOutput.CryptoData DUMMY_CRYPTO_DATA =
new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, new byte[16], 0, 0);
......@@ -461,6 +459,60 @@ public final class SampleQueueTest {
/* decodeOnlyUntilUs= */ 0);
assertThat(result).isEqualTo(RESULT_FORMAT_READ);
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
......@@ -995,11 +1047,11 @@ public final class SampleQueueTest {
private void writeTestDataWithEncryptedSections() {
writeTestData(
ENCRYPTED_SAMPLES_DATA,
ENCRYPTED_SAMPLES_SIZES,
ENCRYPTED_SAMPLES_OFFSETS,
ENCRYPTED_SAMPLE_DATA,
ENCRYPTED_SAMPLE_SIZES,
ENCRYPTED_SAMPLE_OFFSETS,
ENCRYPTED_SAMPLE_TIMESTAMPS,
ENCRYPTED_SAMPLES_FORMATS,
ENCRYPTED_SAMPLE_FORMATS,
ENCRYPTED_SAMPLES_FLAGS);
}
......@@ -1033,7 +1085,12 @@ public final class SampleQueueTest {
/** Writes a single sample to {@code sampleQueue}. */
private void writeSample(byte[] data, long timestampUs, int sampleFlags) {
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 {
}
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);
boolean isKeyFrame = (ENCRYPTED_SAMPLES_FLAGS[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0;
boolean isEncrypted = (ENCRYPTED_SAMPLES_FLAGS[sampleIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0;
......@@ -1216,7 +1273,7 @@ public final class SampleQueueTest {
isEncrypted,
sampleData,
/* 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