Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
SDK
/
exoplayer
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
55ca323c
authored
Jan 20, 2017
by
Oliver Woodman
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
Add upstream.crypto package (and friends).
parent
52d47aa2
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
1033 additions
and
1 deletions
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java
library/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java
library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java
library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java
library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java
library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java
library/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
View file @
55ca323c
...
...
@@ -27,7 +27,9 @@ import java.io.File;
import
java.io.IOException
;
import
java.util.Arrays
;
/** Unit tests for {@link CacheDataSource}. */
/**
* Unit tests for {@link CacheDataSource}.
*/
public
class
CacheDataSourceTest
extends
InstrumentationTestCase
{
private
static
final
byte
[]
TEST_DATA
=
new
byte
[]
{
0
,
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
};
...
...
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java
0 → 100644
View file @
55ca323c
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com
.
google
.
android
.
exoplayer2
.
upstream
.
cache
;
import
android.content.Context
;
import
android.net.Uri
;
import
android.test.AndroidTestCase
;
import
android.test.MoreAsserts
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.testutil.FakeDataSource
;
import
com.google.android.exoplayer2.testutil.TestUtil
;
import
com.google.android.exoplayer2.upstream.DataSink
;
import
com.google.android.exoplayer2.upstream.DataSource
;
import
com.google.android.exoplayer2.upstream.DataSpec
;
import
com.google.android.exoplayer2.upstream.FileDataSource
;
import
com.google.android.exoplayer2.upstream.cache.Cache.CacheException
;
import
com.google.android.exoplayer2.upstream.crypto.AesCipherDataSink
;
import
com.google.android.exoplayer2.upstream.crypto.AesCipherDataSource
;
import
com.google.android.exoplayer2.util.Util
;
import
java.io.File
;
import
java.io.IOException
;
import
java.util.Arrays
;
import
java.util.Random
;
/**
* Additional tests for {@link CacheDataSource}.
*/
public
class
CacheDataSourceTest2
extends
AndroidTestCase
{
private
static
final
String
EXO_CACHE_DIR
=
"exo"
;
private
static
final
int
EXO_CACHE_MAX_FILESIZE
=
128
;
private
static
final
Uri
URI
=
Uri
.
parse
(
"http://test.com/content"
);
private
static
final
String
KEY
=
"key"
;
private
static
final
byte
[]
DATA
=
TestUtil
.
buildTestData
(
8
*
EXO_CACHE_MAX_FILESIZE
+
1
);
// A DataSpec that covers the full file.
private
static
final
DataSpec
FULL
=
new
DataSpec
(
URI
,
0
,
DATA
.
length
,
KEY
);
private
static
final
int
OFFSET_ON_BOUNDARY
=
EXO_CACHE_MAX_FILESIZE
;
// A DataSpec that starts at 0 and extends to a cache file boundary.
private
static
final
DataSpec
END_ON_BOUNDARY
=
new
DataSpec
(
URI
,
0
,
OFFSET_ON_BOUNDARY
,
KEY
);
// A DataSpec that starts on the same boundary and extends to the end of the file.
private
static
final
DataSpec
START_ON_BOUNDARY
=
new
DataSpec
(
URI
,
OFFSET_ON_BOUNDARY
,
DATA
.
length
-
OFFSET_ON_BOUNDARY
,
KEY
);
private
static
final
int
OFFSET_OFF_BOUNDARY
=
EXO_CACHE_MAX_FILESIZE
*
2
+
1
;
// A DataSpec that starts at 0 and extends to just past a cache file boundary.
private
static
final
DataSpec
END_OFF_BOUNDARY
=
new
DataSpec
(
URI
,
0
,
OFFSET_OFF_BOUNDARY
,
KEY
);
// A DataSpec that starts on the same boundary and extends to the end of the file.
private
static
final
DataSpec
START_OFF_BOUNDARY
=
new
DataSpec
(
URI
,
OFFSET_OFF_BOUNDARY
,
DATA
.
length
-
OFFSET_OFF_BOUNDARY
,
KEY
);
public
void
testWithoutEncryption
()
throws
IOException
{
testReads
(
false
);
}
public
void
testWithEncryption
()
throws
IOException
{
testReads
(
true
);
}
private
void
testReads
(
boolean
useEncryption
)
throws
IOException
{
FakeDataSource
upstreamSource
=
buildFakeUpstreamSource
();
CacheDataSource
source
=
buildCacheDataSource
(
getContext
(),
upstreamSource
,
useEncryption
);
// First read, should arrive from upstream.
testRead
(
END_ON_BOUNDARY
,
source
);
assertSingleOpen
(
upstreamSource
,
0
,
OFFSET_ON_BOUNDARY
);
// Second read, should arrive from upstream.
testRead
(
START_OFF_BOUNDARY
,
source
);
assertSingleOpen
(
upstreamSource
,
OFFSET_OFF_BOUNDARY
,
DATA
.
length
);
// Second read, should arrive part from cache and part from upstream.
testRead
(
END_OFF_BOUNDARY
,
source
);
assertSingleOpen
(
upstreamSource
,
OFFSET_ON_BOUNDARY
,
OFFSET_OFF_BOUNDARY
);
// Third read, should arrive from cache.
testRead
(
FULL
,
source
);
assertNoOpen
(
upstreamSource
);
// Various reads, should all arrive from cache.
testRead
(
FULL
,
source
);
assertNoOpen
(
upstreamSource
);
testRead
(
START_ON_BOUNDARY
,
source
);
assertNoOpen
(
upstreamSource
);
testRead
(
END_ON_BOUNDARY
,
source
);
assertNoOpen
(
upstreamSource
);
testRead
(
START_OFF_BOUNDARY
,
source
);
assertNoOpen
(
upstreamSource
);
testRead
(
END_OFF_BOUNDARY
,
source
);
assertNoOpen
(
upstreamSource
);
}
private
void
testRead
(
DataSpec
dataSpec
,
CacheDataSource
source
)
throws
IOException
{
byte
[]
scratch
=
new
byte
[
4096
];
Random
random
=
new
Random
(
0
);
source
.
open
(
dataSpec
);
int
position
=
(
int
)
dataSpec
.
absoluteStreamPosition
;
int
bytesRead
=
0
;
while
(
bytesRead
!=
C
.
RESULT_END_OF_INPUT
)
{
int
maxBytesToRead
=
random
.
nextInt
(
scratch
.
length
)
+
1
;
bytesRead
=
source
.
read
(
scratch
,
0
,
maxBytesToRead
);
if
(
bytesRead
!=
C
.
RESULT_END_OF_INPUT
)
{
MoreAsserts
.
assertEquals
(
Arrays
.
copyOfRange
(
DATA
,
position
,
position
+
bytesRead
),
Arrays
.
copyOf
(
scratch
,
bytesRead
));
position
+=
bytesRead
;
}
}
source
.
close
();
}
/**
* Asserts that a single {@link DataSource#open(DataSpec)} call has been made to the upstream
* source, with the specified start (inclusive) and end (exclusive) positions.
*/
private
void
assertSingleOpen
(
FakeDataSource
upstreamSource
,
int
start
,
int
end
)
{
DataSpec
[]
openedDataSpecs
=
upstreamSource
.
getAndClearOpenedDataSpecs
();
assertEquals
(
1
,
openedDataSpecs
.
length
);
assertEquals
(
start
,
openedDataSpecs
[
0
].
position
);
assertEquals
(
start
,
openedDataSpecs
[
0
].
absoluteStreamPosition
);
assertEquals
(
end
-
start
,
openedDataSpecs
[
0
].
length
);
}
/**
* Asserts that the upstream source was not opened.
*/
private
void
assertNoOpen
(
FakeDataSource
upstreamSource
)
{
DataSpec
[]
openedDataSpecs
=
upstreamSource
.
getAndClearOpenedDataSpecs
();
assertEquals
(
0
,
openedDataSpecs
.
length
);
}
private
static
FakeDataSource
buildFakeUpstreamSource
()
{
return
new
FakeDataSource
.
Builder
().
appendReadData
(
DATA
).
build
();
}
private
static
CacheDataSource
buildCacheDataSource
(
Context
context
,
DataSource
upstreamSource
,
boolean
useAesEncryption
)
throws
CacheException
{
File
cacheDir
=
context
.
getExternalCacheDir
();
Cache
cache
=
new
SimpleCache
(
new
File
(
cacheDir
,
EXO_CACHE_DIR
),
new
NoOpCacheEvictor
());
emptyCache
(
cache
);
// Source and cipher
final
String
secretKey
=
"testKey:12345678"
;
DataSource
file
=
new
FileDataSource
();
DataSource
cacheReadDataSource
=
useAesEncryption
?
new
AesCipherDataSource
(
Util
.
getUtf8Bytes
(
secretKey
),
file
)
:
file
;
// Sink and cipher
CacheDataSink
cacheSink
=
new
CacheDataSink
(
cache
,
EXO_CACHE_MAX_FILESIZE
);
byte
[]
scratch
=
new
byte
[
3897
];
DataSink
cacheWriteDataSink
=
useAesEncryption
?
new
AesCipherDataSink
(
Util
.
getUtf8Bytes
(
secretKey
),
cacheSink
,
scratch
)
:
cacheSink
;
return
new
CacheDataSource
(
cache
,
upstreamSource
,
cacheReadDataSource
,
cacheWriteDataSink
,
CacheDataSource
.
FLAG_BLOCK_ON_CACHE
,
null
);
// eventListener
}
private
static
void
emptyCache
(
Cache
cache
)
throws
CacheException
{
for
(
String
key
:
cache
.
getKeys
())
{
for
(
CacheSpan
span
:
cache
.
getCachedSpans
(
key
))
{
cache
.
removeSpan
(
span
);
}
}
// Sanity check that the cache really is empty now.
assertTrue
(
cache
.
getKeys
().
isEmpty
());
}
}
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java
0 → 100644
View file @
55ca323c
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com
.
google
.
android
.
exoplayer2
.
upstream
.
cache
;
import
android.test.InstrumentationTestCase
;
import
com.google.android.exoplayer2.extractor.ChunkIndex
;
import
com.google.android.exoplayer2.testutil.TestUtil
;
import
java.io.File
;
import
java.io.IOException
;
import
org.mockito.Mock
;
/**
* Tests for {@link CachedRegionTracker}.
*/
public
final
class
CachedRegionTrackerTest
extends
InstrumentationTestCase
{
private
static
final
String
CACHE_KEY
=
"abc"
;
private
static
final
long
MS_IN_US
=
1000
;
// 5 chunks, each 20 bytes long and 100 ms long.
private
static
final
ChunkIndex
CHUNK_INDEX
=
new
ChunkIndex
(
new
int
[]
{
20
,
20
,
20
,
20
,
20
},
new
long
[]
{
100
,
120
,
140
,
160
,
180
},
new
long
[]
{
100
*
MS_IN_US
,
100
*
MS_IN_US
,
100
*
MS_IN_US
,
100
*
MS_IN_US
,
100
*
MS_IN_US
},
new
long
[]
{
0
,
100
*
MS_IN_US
,
200
*
MS_IN_US
,
300
*
MS_IN_US
,
400
*
MS_IN_US
});
@Mock
private
Cache
cache
;
private
CachedRegionTracker
tracker
;
private
CachedContentIndex
index
;
private
File
cacheDir
;
@Override
protected
void
setUp
()
throws
Exception
{
TestUtil
.
setUpMockito
(
this
);
tracker
=
new
CachedRegionTracker
(
cache
,
CACHE_KEY
,
CHUNK_INDEX
);
cacheDir
=
TestUtil
.
createTempFolder
(
getInstrumentation
().
getContext
());
index
=
new
CachedContentIndex
(
cacheDir
);
}
@Override
protected
void
tearDown
()
throws
Exception
{
TestUtil
.
recursiveDelete
(
cacheDir
);
}
public
void
testGetRegion_noSpansInCache
()
{
assertEquals
(
CachedRegionTracker
.
NOT_CACHED
,
tracker
.
getRegionEndTimeMs
(
100
));
assertEquals
(
CachedRegionTracker
.
NOT_CACHED
,
tracker
.
getRegionEndTimeMs
(
150
));
}
public
void
testGetRegion_fullyCached
()
throws
Exception
{
tracker
.
onSpanAdded
(
cache
,
newCacheSpan
(
100
,
100
));
assertEquals
(
CachedRegionTracker
.
CACHED_TO_END
,
tracker
.
getRegionEndTimeMs
(
101
));
assertEquals
(
CachedRegionTracker
.
CACHED_TO_END
,
tracker
.
getRegionEndTimeMs
(
121
));
}
public
void
testGetRegion_partiallyCached
()
throws
Exception
{
tracker
.
onSpanAdded
(
cache
,
newCacheSpan
(
100
,
40
));
assertEquals
(
200
,
tracker
.
getRegionEndTimeMs
(
101
));
assertEquals
(
200
,
tracker
.
getRegionEndTimeMs
(
121
));
}
public
void
testGetRegion_multipleSpanAddsJoinedCorrectly
()
throws
Exception
{
tracker
.
onSpanAdded
(
cache
,
newCacheSpan
(
100
,
20
));
tracker
.
onSpanAdded
(
cache
,
newCacheSpan
(
120
,
20
));
assertEquals
(
200
,
tracker
.
getRegionEndTimeMs
(
101
));
assertEquals
(
200
,
tracker
.
getRegionEndTimeMs
(
121
));
}
public
void
testGetRegion_fullyCachedThenPartiallyRemoved
()
throws
Exception
{
// Start with the full stream in cache.
tracker
.
onSpanAdded
(
cache
,
newCacheSpan
(
100
,
100
));
// Remove the middle bit.
tracker
.
onSpanRemoved
(
cache
,
newCacheSpan
(
140
,
40
));
assertEquals
(
200
,
tracker
.
getRegionEndTimeMs
(
101
));
assertEquals
(
200
,
tracker
.
getRegionEndTimeMs
(
121
));
assertEquals
(
CachedRegionTracker
.
CACHED_TO_END
,
tracker
.
getRegionEndTimeMs
(
181
));
}
public
void
testGetRegion_subchunkEstimation
()
throws
Exception
{
tracker
.
onSpanAdded
(
cache
,
newCacheSpan
(
100
,
10
));
assertEquals
(
50
,
tracker
.
getRegionEndTimeMs
(
101
));
assertEquals
(
CachedRegionTracker
.
NOT_CACHED
,
tracker
.
getRegionEndTimeMs
(
111
));
}
private
CacheSpan
newCacheSpan
(
int
position
,
int
length
)
throws
IOException
{
return
SimpleCacheSpanTest
.
createCacheSpan
(
index
,
cacheDir
,
CACHE_KEY
,
position
,
length
,
0
);
}
}
library/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java
0 → 100644
View file @
55ca323c
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com
.
google
.
android
.
exoplayer2
.
upstream
.
crypto
;
import
com.google.android.exoplayer2.testutil.TestUtil
;
import
com.google.android.exoplayer2.util.Util
;
import
java.util.Random
;
import
javax.crypto.Cipher
;
import
junit.framework.TestCase
;
/**
* Unit tests for {@link AesFlushingCipher}.
*/
public
class
AesFlushingCipherTest
extends
TestCase
{
private
static
final
int
DATA_LENGTH
=
65536
;
private
static
final
byte
[]
KEY
=
Util
.
getUtf8Bytes
(
"testKey:12345678"
);
private
static
final
long
NONCE
=
0
;
private
static
final
long
START_OFFSET
=
11
;
private
static
final
long
RANDOM_SEED
=
0x12345678
;
private
AesFlushingCipher
encryptCipher
;
private
AesFlushingCipher
decryptCipher
;
@Override
protected
void
setUp
()
{
encryptCipher
=
new
AesFlushingCipher
(
Cipher
.
ENCRYPT_MODE
,
KEY
,
NONCE
,
START_OFFSET
);
decryptCipher
=
new
AesFlushingCipher
(
Cipher
.
DECRYPT_MODE
,
KEY
,
NONCE
,
START_OFFSET
);
}
@Override
protected
void
tearDown
()
{
encryptCipher
=
null
;
decryptCipher
=
null
;
}
private
long
getMaxUnchangedBytesAllowedPostEncryption
(
long
length
)
{
// Assuming that not more than 10% of the resultant bytes should be identical.
// The value of 10% is arbitrary, ciphers standards do not name a value.
return
length
/
10
;
}
// Count the number of bytes that do not match.
private
int
getDifferingByteCount
(
byte
[]
data1
,
byte
[]
data2
,
int
startOffset
)
{
int
count
=
0
;
for
(
int
i
=
startOffset
;
i
<
data1
.
length
;
i
++)
{
if
(
data1
[
i
]
!=
data2
[
i
])
{
count
++;
}
}
return
count
;
}
// Count the number of bytes that do not match.
private
int
getDifferingByteCount
(
byte
[]
data1
,
byte
[]
data2
)
{
return
getDifferingByteCount
(
data1
,
data2
,
0
);
}
// Test a single encrypt and decrypt call
public
void
testSingle
()
{
byte
[]
reference
=
TestUtil
.
buildTestData
(
DATA_LENGTH
);
byte
[]
data
=
reference
.
clone
();
encryptCipher
.
updateInPlace
(
data
,
0
,
data
.
length
);
int
unchangedByteCount
=
data
.
length
-
getDifferingByteCount
(
reference
,
data
);
assertTrue
(
unchangedByteCount
<=
getMaxUnchangedBytesAllowedPostEncryption
(
data
.
length
));
decryptCipher
.
updateInPlace
(
data
,
0
,
data
.
length
);
int
differingByteCount
=
getDifferingByteCount
(
reference
,
data
);
assertEquals
(
0
,
differingByteCount
);
}
// Test several encrypt and decrypt calls, each aligned on a 16 byte block size
public
void
testAligned
()
{
byte
[]
reference
=
TestUtil
.
buildTestData
(
DATA_LENGTH
);
byte
[]
data
=
reference
.
clone
();
Random
random
=
new
Random
(
RANDOM_SEED
);
int
offset
=
0
;
while
(
offset
<
data
.
length
)
{
int
bytes
=
(
1
+
random
.
nextInt
(
50
))
*
16
;
bytes
=
Math
.
min
(
bytes
,
data
.
length
-
offset
);
assertEquals
(
0
,
bytes
%
16
);
encryptCipher
.
updateInPlace
(
data
,
offset
,
bytes
);
offset
+=
bytes
;
}
int
unchangedByteCount
=
data
.
length
-
getDifferingByteCount
(
reference
,
data
);
assertTrue
(
unchangedByteCount
<=
getMaxUnchangedBytesAllowedPostEncryption
(
data
.
length
));
offset
=
0
;
while
(
offset
<
data
.
length
)
{
int
bytes
=
(
1
+
random
.
nextInt
(
50
))
*
16
;
bytes
=
Math
.
min
(
bytes
,
data
.
length
-
offset
);
assertEquals
(
0
,
bytes
%
16
);
decryptCipher
.
updateInPlace
(
data
,
offset
,
bytes
);
offset
+=
bytes
;
}
int
differingByteCount
=
getDifferingByteCount
(
reference
,
data
);
assertEquals
(
0
,
differingByteCount
);
}
// Test several encrypt and decrypt calls, not aligned on block boundary
public
void
testUnAligned
()
{
byte
[]
reference
=
TestUtil
.
buildTestData
(
DATA_LENGTH
);
byte
[]
data
=
reference
.
clone
();
Random
random
=
new
Random
(
RANDOM_SEED
);
// Encrypt
int
offset
=
0
;
while
(
offset
<
data
.
length
)
{
int
bytes
=
1
+
random
.
nextInt
(
4095
);
bytes
=
Math
.
min
(
bytes
,
data
.
length
-
offset
);
encryptCipher
.
updateInPlace
(
data
,
offset
,
bytes
);
offset
+=
bytes
;
}
int
unchangedByteCount
=
data
.
length
-
getDifferingByteCount
(
reference
,
data
);
assertTrue
(
unchangedByteCount
<=
getMaxUnchangedBytesAllowedPostEncryption
(
data
.
length
));
offset
=
0
;
while
(
offset
<
data
.
length
)
{
int
bytes
=
1
+
random
.
nextInt
(
4095
);
bytes
=
Math
.
min
(
bytes
,
data
.
length
-
offset
);
decryptCipher
.
updateInPlace
(
data
,
offset
,
bytes
);
offset
+=
bytes
;
}
int
differingByteCount
=
getDifferingByteCount
(
reference
,
data
);
assertEquals
(
0
,
differingByteCount
);
}
// Test decryption starting from the middle of an encrypted block
public
void
testMidJoin
()
{
byte
[]
reference
=
TestUtil
.
buildTestData
(
DATA_LENGTH
);
byte
[]
data
=
reference
.
clone
();
Random
random
=
new
Random
(
RANDOM_SEED
);
// Encrypt
int
offset
=
0
;
while
(
offset
<
data
.
length
)
{
int
bytes
=
1
+
random
.
nextInt
(
4095
);
bytes
=
Math
.
min
(
bytes
,
data
.
length
-
offset
);
encryptCipher
.
updateInPlace
(
data
,
offset
,
bytes
);
offset
+=
bytes
;
}
// Verify
int
unchangedByteCount
=
data
.
length
-
getDifferingByteCount
(
reference
,
data
);
assertTrue
(
unchangedByteCount
<=
getMaxUnchangedBytesAllowedPostEncryption
(
data
.
length
));
// Setup decryption from random location
offset
=
random
.
nextInt
(
4096
);
decryptCipher
=
new
AesFlushingCipher
(
Cipher
.
DECRYPT_MODE
,
KEY
,
NONCE
,
offset
+
START_OFFSET
);
int
remainingLength
=
data
.
length
-
offset
;
int
originalOffset
=
offset
;
// Decrypt
while
(
remainingLength
>
0
)
{
int
bytes
=
1
+
random
.
nextInt
(
4095
);
bytes
=
Math
.
min
(
bytes
,
remainingLength
);
decryptCipher
.
updateInPlace
(
data
,
offset
,
bytes
);
offset
+=
bytes
;
remainingLength
-=
bytes
;
}
// Verify
int
differingByteCount
=
getDifferingByteCount
(
reference
,
data
,
originalOffset
);
assertEquals
(
0
,
differingByteCount
);
}
}
library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java
0 → 100644
View file @
55ca323c
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com
.
google
.
android
.
exoplayer2
.
upstream
.
cache
;
import
android.util.Log
;
import
com.google.android.exoplayer2.extractor.ChunkIndex
;
import
java.util.Arrays
;
import
java.util.Iterator
;
import
java.util.NavigableSet
;
import
java.util.TreeSet
;
/**
* Utility class for efficiently tracking regions of data that are stored in a {@link Cache}
* for a given cache key.
*/
public
final
class
CachedRegionTracker
implements
Cache
.
Listener
{
private
static
final
String
TAG
=
"CachedRegionTracker"
;
public
static
final
int
NOT_CACHED
=
-
1
;
public
static
final
int
CACHED_TO_END
=
-
2
;
private
final
Cache
cache
;
private
final
String
cacheKey
;
private
final
ChunkIndex
chunkIndex
;
private
final
TreeSet
<
Region
>
regions
;
private
final
Region
lookupRegion
;
public
CachedRegionTracker
(
Cache
cache
,
String
cacheKey
,
ChunkIndex
chunkIndex
)
{
this
.
cache
=
cache
;
this
.
cacheKey
=
cacheKey
;
this
.
chunkIndex
=
chunkIndex
;
this
.
regions
=
new
TreeSet
<>();
this
.
lookupRegion
=
new
Region
(
0
,
0
);
synchronized
(
this
)
{
NavigableSet
<
CacheSpan
>
cacheSpans
=
cache
.
addListener
(
cacheKey
,
this
);
if
(
cacheSpans
!=
null
)
{
// Merge the spans into regions. mergeSpan is more efficient when merging from high to low,
// which is why a descending iterator is used here.
Iterator
<
CacheSpan
>
spanIterator
=
cacheSpans
.
descendingIterator
();
while
(
spanIterator
.
hasNext
())
{
CacheSpan
span
=
spanIterator
.
next
();
mergeSpan
(
span
);
}
}
}
}
public
void
release
()
{
cache
.
removeListener
(
cacheKey
,
this
);
}
/**
* When provided with a byte offset, this method locates the cached region within which the
* offset falls, and returns the approximate end position in milliseconds of that region. If the
* byte offset does not fall within a cached region then {@link #NOT_CACHED} is returned.
* If the cached region extends to the end of the stream, {@link #CACHED_TO_END} is returned.
*
* @param byteOffset The byte offset in the underlying stream.
* @return The end position of the corresponding cache region, {@link #NOT_CACHED}, or
* {@link #CACHED_TO_END}.
*/
public
synchronized
int
getRegionEndTimeMs
(
long
byteOffset
)
{
lookupRegion
.
startOffset
=
byteOffset
;
Region
floorRegion
=
regions
.
floor
(
lookupRegion
);
if
(
floorRegion
==
null
||
byteOffset
>
floorRegion
.
endOffset
||
floorRegion
.
endOffsetIndex
==
-
1
)
{
return
NOT_CACHED
;
}
int
index
=
floorRegion
.
endOffsetIndex
;
if
(
index
==
chunkIndex
.
length
-
1
&&
floorRegion
.
endOffset
==
(
chunkIndex
.
offsets
[
index
]
+
chunkIndex
.
sizes
[
index
]))
{
return
CACHED_TO_END
;
}
long
segmentFractionUs
=
(
chunkIndex
.
durationsUs
[
index
]
*
(
floorRegion
.
endOffset
-
chunkIndex
.
offsets
[
index
]))
/
chunkIndex
.
sizes
[
index
];
return
(
int
)
((
chunkIndex
.
timesUs
[
index
]
+
segmentFractionUs
)
/
1000
);
}
@Override
public
synchronized
void
onSpanAdded
(
Cache
cache
,
CacheSpan
span
)
{
mergeSpan
(
span
);
}
@Override
public
synchronized
void
onSpanRemoved
(
Cache
cache
,
CacheSpan
span
)
{
Region
removedRegion
=
new
Region
(
span
.
position
,
span
.
position
+
span
.
length
);
// Look up a region this span falls into.
Region
floorRegion
=
regions
.
floor
(
removedRegion
);
if
(
floorRegion
==
null
)
{
Log
.
e
(
TAG
,
"Removed a span we were not aware of"
);
return
;
}
// Remove it.
regions
.
remove
(
floorRegion
);
// Add new floor and ceiling regions, if necessary.
if
(
floorRegion
.
startOffset
<
removedRegion
.
startOffset
)
{
Region
newFloorRegion
=
new
Region
(
floorRegion
.
startOffset
,
removedRegion
.
startOffset
);
int
index
=
Arrays
.
binarySearch
(
chunkIndex
.
offsets
,
newFloorRegion
.
endOffset
);
newFloorRegion
.
endOffsetIndex
=
index
<
0
?
-
index
-
2
:
index
;
regions
.
add
(
newFloorRegion
);
}
if
(
floorRegion
.
endOffset
>
removedRegion
.
endOffset
)
{
Region
newCeilingRegion
=
new
Region
(
removedRegion
.
endOffset
+
1
,
floorRegion
.
endOffset
);
newCeilingRegion
.
endOffsetIndex
=
floorRegion
.
endOffsetIndex
;
regions
.
add
(
newCeilingRegion
);
}
}
@Override
public
void
onSpanTouched
(
Cache
cache
,
CacheSpan
oldSpan
,
CacheSpan
newSpan
)
{
// Do nothing.
}
private
void
mergeSpan
(
CacheSpan
span
)
{
Region
newRegion
=
new
Region
(
span
.
position
,
span
.
position
+
span
.
length
);
Region
floorRegion
=
regions
.
floor
(
newRegion
);
Region
ceilingRegion
=
regions
.
ceiling
(
newRegion
);
boolean
floorConnects
=
regionsConnect
(
floorRegion
,
newRegion
);
boolean
ceilingConnects
=
regionsConnect
(
newRegion
,
ceilingRegion
);
if
(
ceilingConnects
)
{
if
(
floorConnects
)
{
// Extend floorRegion to cover both newRegion and ceilingRegion.
floorRegion
.
endOffset
=
ceilingRegion
.
endOffset
;
floorRegion
.
endOffsetIndex
=
ceilingRegion
.
endOffsetIndex
;
}
else
{
// Extend newRegion to cover ceilingRegion. Add it.
newRegion
.
endOffset
=
ceilingRegion
.
endOffset
;
newRegion
.
endOffsetIndex
=
ceilingRegion
.
endOffsetIndex
;
regions
.
add
(
newRegion
);
}
regions
.
remove
(
ceilingRegion
);
}
else
if
(
floorConnects
)
{
// Extend floorRegion to the right to cover newRegion.
floorRegion
.
endOffset
=
newRegion
.
endOffset
;
int
index
=
floorRegion
.
endOffsetIndex
;
while
(
index
<
chunkIndex
.
length
-
1
&&
(
chunkIndex
.
offsets
[
index
+
1
]
<=
floorRegion
.
endOffset
))
{
index
++;
}
floorRegion
.
endOffsetIndex
=
index
;
}
else
{
// This is a new region.
int
index
=
Arrays
.
binarySearch
(
chunkIndex
.
offsets
,
newRegion
.
endOffset
);
newRegion
.
endOffsetIndex
=
index
<
0
?
-
index
-
2
:
index
;
regions
.
add
(
newRegion
);
}
}
private
boolean
regionsConnect
(
Region
lower
,
Region
upper
)
{
return
lower
!=
null
&&
upper
!=
null
&&
lower
.
endOffset
==
upper
.
startOffset
;
}
private
static
class
Region
implements
Comparable
<
Region
>
{
/**
* The first byte of the region (inclusive).
*/
public
long
startOffset
;
/**
* End offset of the region (exclusive).
*/
public
long
endOffset
;
/**
* The index in chunkIndex that contains the end offset. May be -1 if the end offset comes
* before the start of the first media chunk (i.e. if the end offset is within the stream
* header).
*/
public
int
endOffsetIndex
;
public
Region
(
long
position
,
long
endOffset
)
{
this
.
startOffset
=
position
;
this
.
endOffset
=
endOffset
;
}
@Override
public
int
compareTo
(
Region
another
)
{
return
startOffset
<
another
.
startOffset
?
-
1
:
startOffset
==
another
.
startOffset
?
0
:
1
;
}
}
}
library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java
0 → 100644
View file @
55ca323c
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com
.
google
.
android
.
exoplayer2
.
upstream
.
crypto
;
import
com.google.android.exoplayer2.upstream.DataSink
;
import
com.google.android.exoplayer2.upstream.DataSpec
;
import
java.io.IOException
;
import
javax.crypto.Cipher
;
/**
* A wrapping {@link DataSink} that encrypts the data being consumed.
*/
public
final
class
AesCipherDataSink
implements
DataSink
{
private
final
DataSink
wrappedDataSink
;
private
final
byte
[]
secretKey
;
private
final
byte
[]
scratch
;
private
AesFlushingCipher
cipher
;
/**
* Create an instance whose {@code write} methods have the side effect of overwriting the input
* {@code data}. Use this constructor for maximum efficiency in the case that there is no
* requirement for the input data arrays to remain unchanged.
*
* @param secretKey The key data.
* @param wrappedDataSink The wrapped {@link DataSink}.
*/
public
AesCipherDataSink
(
byte
[]
secretKey
,
DataSink
wrappedDataSink
)
{
this
(
secretKey
,
wrappedDataSink
,
null
);
}
/**
* Create an instance whose {@code write} methods are free of side effects. Use this constructor
* when the input data arrays are required to remain unchanged.
*
* @param secretKey The key data.
* @param wrappedDataSink The wrapped {@link DataSink}.
* @param scratch Scratch space. Data is decrypted into this array before being written to the
* wrapped {@link DataSink}. It should be of appropriate size for the expected writes. If a
* write is larger than the size of this array the write will still succeed, but multiple
* cipher calls will be required to complete the operation.
*/
public
AesCipherDataSink
(
byte
[]
secretKey
,
DataSink
wrappedDataSink
,
byte
[]
scratch
)
{
this
.
wrappedDataSink
=
wrappedDataSink
;
this
.
secretKey
=
secretKey
;
this
.
scratch
=
scratch
;
}
@Override
public
void
open
(
DataSpec
dataSpec
)
throws
IOException
{
wrappedDataSink
.
open
(
dataSpec
);
long
nonce
=
CryptoUtil
.
getFNV64Hash
(
dataSpec
.
key
);
cipher
=
new
AesFlushingCipher
(
Cipher
.
ENCRYPT_MODE
,
secretKey
,
nonce
,
dataSpec
.
absoluteStreamPosition
);
}
@Override
public
void
write
(
byte
[]
data
,
int
offset
,
int
length
)
throws
IOException
{
if
(
scratch
==
null
)
{
// In-place mode. Writes over the input data.
cipher
.
updateInPlace
(
data
,
offset
,
length
);
wrappedDataSink
.
write
(
data
,
offset
,
length
);
}
else
{
// Use scratch space. The original data remains intact.
int
bytesProcessed
=
0
;
while
(
bytesProcessed
<
length
)
{
int
bytesToProcess
=
Math
.
min
(
length
-
bytesProcessed
,
scratch
.
length
);
cipher
.
update
(
data
,
offset
+
bytesProcessed
,
bytesToProcess
,
scratch
,
0
);
wrappedDataSink
.
write
(
scratch
,
0
,
bytesToProcess
);
bytesProcessed
+=
bytesToProcess
;
}
}
}
@Override
public
void
close
()
throws
IOException
{
cipher
=
null
;
wrappedDataSink
.
close
();
}
}
library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java
0 → 100644
View file @
55ca323c
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com
.
google
.
android
.
exoplayer2
.
upstream
.
crypto
;
import
android.net.Uri
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.upstream.DataSource
;
import
com.google.android.exoplayer2.upstream.DataSpec
;
import
java.io.IOException
;
import
javax.crypto.Cipher
;
/**
* A {@link DataSource} that decrypts the data read from an upstream source.
*/
public
final
class
AesCipherDataSource
implements
DataSource
{
private
final
DataSource
upstream
;
private
final
byte
[]
secretKey
;
private
AesFlushingCipher
cipher
;
public
AesCipherDataSource
(
byte
[]
secretKey
,
DataSource
upstream
)
{
this
.
upstream
=
upstream
;
this
.
secretKey
=
secretKey
;
}
@Override
public
long
open
(
DataSpec
dataSpec
)
throws
IOException
{
long
dataLength
=
upstream
.
open
(
dataSpec
);
long
nonce
=
CryptoUtil
.
getFNV64Hash
(
dataSpec
.
key
);
cipher
=
new
AesFlushingCipher
(
Cipher
.
DECRYPT_MODE
,
secretKey
,
nonce
,
dataSpec
.
absoluteStreamPosition
);
return
dataLength
;
}
@Override
public
int
read
(
byte
[]
data
,
int
offset
,
int
readLength
)
throws
IOException
{
if
(
readLength
==
0
)
{
return
0
;
}
int
read
=
upstream
.
read
(
data
,
offset
,
readLength
);
if
(
read
==
C
.
RESULT_END_OF_INPUT
)
{
return
C
.
RESULT_END_OF_INPUT
;
}
cipher
.
updateInPlace
(
data
,
offset
,
read
);
return
read
;
}
@Override
public
void
close
()
throws
IOException
{
cipher
=
null
;
upstream
.
close
();
}
@Override
public
Uri
getUri
()
{
return
upstream
.
getUri
();
}
}
library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java
0 → 100644
View file @
55ca323c
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com
.
google
.
android
.
exoplayer2
.
upstream
.
crypto
;
import
com.google.android.exoplayer2.util.Assertions
;
import
java.nio.ByteBuffer
;
import
java.security.InvalidAlgorithmParameterException
;
import
java.security.InvalidKeyException
;
import
java.security.NoSuchAlgorithmException
;
import
javax.crypto.Cipher
;
import
javax.crypto.NoSuchPaddingException
;
import
javax.crypto.ShortBufferException
;
import
javax.crypto.spec.IvParameterSpec
;
import
javax.crypto.spec.SecretKeySpec
;
/**
* A flushing variant of a AES/CTR/NoPadding {@link Cipher}.
*
* Unlike a regular {@link Cipher}, the update methods of this class are guaranteed to process all
* of the bytes input (and hence output the same number of bytes).
*/
public
final
class
AesFlushingCipher
{
private
final
Cipher
cipher
;
private
final
int
blockSize
;
private
final
byte
[]
zerosBlock
;
private
final
byte
[]
flushedBlock
;
private
int
pendingXorBytes
;
public
AesFlushingCipher
(
int
mode
,
byte
[]
secretKey
,
long
nonce
,
long
offset
)
{
try
{
cipher
=
Cipher
.
getInstance
(
"AES/CTR/NoPadding"
);
blockSize
=
cipher
.
getBlockSize
();
zerosBlock
=
new
byte
[
blockSize
];
flushedBlock
=
new
byte
[
blockSize
];
long
counter
=
offset
/
blockSize
;
int
startPadding
=
(
int
)
(
offset
%
blockSize
);
cipher
.
init
(
mode
,
new
SecretKeySpec
(
secretKey
,
cipher
.
getAlgorithm
().
split
(
"/"
)[
0
]),
new
IvParameterSpec
(
getInitializationVector
(
nonce
,
counter
)));
if
(
startPadding
!=
0
)
{
updateInPlace
(
new
byte
[
startPadding
],
0
,
startPadding
);
}
}
catch
(
NoSuchAlgorithmException
|
NoSuchPaddingException
|
InvalidKeyException
|
InvalidAlgorithmParameterException
e
)
{
// Should never happen.
throw
new
RuntimeException
(
e
);
}
}
public
void
updateInPlace
(
byte
[]
data
,
int
offset
,
int
length
)
{
update
(
data
,
offset
,
length
,
data
,
offset
);
}
public
void
update
(
byte
[]
in
,
int
inOffset
,
int
length
,
byte
[]
out
,
int
outOffset
)
{
// If we previously flushed the cipher by inputting zeros up to a block boundary, then we need
// to manually transform the data that actually ended the block. See the comment below for more
// details.
while
(
pendingXorBytes
>
0
)
{
out
[
outOffset
]
=
(
byte
)
(
in
[
inOffset
]
^
flushedBlock
[
blockSize
-
pendingXorBytes
]);
outOffset
++;
inOffset
++;
pendingXorBytes
--;
length
--;
if
(
length
==
0
)
{
return
;
}
}
// Do the bulk of the update.
int
written
=
nonFlushingUpdate
(
in
,
inOffset
,
length
,
out
,
outOffset
);
if
(
length
==
written
)
{
return
;
}
// We need to finish the block to flush out the remaining bytes. We do so by inputting zeros,
// so that the corresponding bytes output by the cipher are those that would have been XORed
// against the real end-of-block data to transform it. We store these bytes so that we can
// perform the transformation manually in the case of a subsequent call to this method with
// the real data.
int
bytesToFlush
=
length
-
written
;
Assertions
.
checkState
(
bytesToFlush
<
blockSize
);
outOffset
+=
written
;
pendingXorBytes
=
blockSize
-
bytesToFlush
;
written
=
nonFlushingUpdate
(
zerosBlock
,
0
,
pendingXorBytes
,
flushedBlock
,
0
);
Assertions
.
checkState
(
written
==
blockSize
);
// The first part of xorBytes contains the flushed data, which we copy out. The remainder
// contains the bytes that will be needed for manual transformation in a subsequent call.
for
(
int
i
=
0
;
i
<
bytesToFlush
;
i
++)
{
out
[
outOffset
++]
=
flushedBlock
[
i
];
}
}
private
int
nonFlushingUpdate
(
byte
[]
in
,
int
inOffset
,
int
length
,
byte
[]
out
,
int
outOffset
)
{
try
{
return
cipher
.
update
(
in
,
inOffset
,
length
,
out
,
outOffset
);
}
catch
(
ShortBufferException
e
)
{
// Should never happen.
throw
new
RuntimeException
(
e
);
}
}
private
byte
[]
getInitializationVector
(
long
nonce
,
long
counter
)
{
return
ByteBuffer
.
allocate
(
16
).
putLong
(
nonce
).
putLong
(
counter
).
array
();
}
}
library/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java
0 → 100644
View file @
55ca323c
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
com
.
google
.
android
.
exoplayer2
.
upstream
.
crypto
;
/**
* Utility functions for the crypto package.
*/
/* package */
final
class
CryptoUtil
{
private
CryptoUtil
()
{}
/**
* Returns the hash value of the input as a long using the 64 bit FNV-1a hash function. The hash
* values produced by this function are less likely to collide than those produced by
* {@link #hashCode()}.
*/
public
static
long
getFNV64Hash
(
String
input
)
{
if
(
input
==
null
)
{
return
0
;
}
long
hash
=
0
;
for
(
int
i
=
0
;
i
<
input
.
length
();
i
++)
{
hash
^=
input
.
charAt
(
i
);
// This is equivalent to hash *= 0x100000001b3 (the FNV magic prime number).
hash
+=
(
hash
<<
1
)
+
(
hash
<<
4
)
+
(
hash
<<
5
)
+
(
hash
<<
7
)
+
(
hash
<<
8
)
+
(
hash
<<
40
);
}
return
hash
;
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment