From d1e4a63ae0df09c45f623edac6579b967649d5e8 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 17 Feb 2020 14:23:06 +0000 Subject: [PATCH] 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 --- RELEASENOTES.md | 6 ++ .../exoplayer2/decoder/CryptoInfo.java | 44 +++++----- .../exoplayer2/decoder/CryptoInfoTest.java | 31 ------- .../exoplayer2/source/SampleDataQueue.java | 20 +++-- .../exoplayer2/source/SampleQueueTest.java | 87 +++++++++++++++---- 5 files changed, 114 insertions(+), 74 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e61c73e34ae..337acc6e885 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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 diff --git a/library/common/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java index d8d3c5be432..c3cebb970dd 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java @@ -27,27 +27,41 @@ 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 void copyTo(android.media.MediaCodec.CryptoInfo 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 void increaseClearDataFirstSubSampleBy(int count) { 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; } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/decoder/CryptoInfoTest.java b/library/common/src/test/java/com/google/android/exoplayer2/decoder/CryptoInfoTest.java index 3225ca22a52..2426d473e90 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/decoder/CryptoInfoTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/decoder/CryptoInfoTest.java @@ -65,35 +65,4 @@ public void increaseClearDataFirstSubSampleBy_withSharedClearDataPointer_setsVal 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); - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java index fbe48ea6c4f..c29df403ecc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java @@ -17,6 +17,7 @@ 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 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 @@ private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder ex 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 @@ private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder ex } // 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 @@ private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder ex // 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); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index a8a1efc49c6..649be2d77e4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -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.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 void testAllowPlaceholderSessionPopulatesDrmSession() { /* 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 mockPlaceholderDrmSession = + (DrmSession) 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 @@ private void writeTestData() { 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 @@ private void writeFormat(Format format) { /** 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 @@ private void assertReadFormat(boolean formatRequired, Format format) { } 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 @@ private void assertReadEncryptedSample(int sampleIndex) { isEncrypted, sampleData, /* offset= */ 0, - ENCRYPTED_SAMPLES_SIZES[sampleIndex] - (isEncrypted ? 2 : 0)); + ENCRYPTED_SAMPLE_SIZES[sampleIndex] - (isEncrypted ? 2 : 0)); } /**