Skip to content

Commit 0fa652c

Browse files
authored
Fix CBC decryption when originalLength is wrong (#592)
1 parent 60013b8 commit 0fa652c

File tree

3 files changed

+102
-39
lines changed

3 files changed

+102
-39
lines changed

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,18 @@ All notable changes to this project will be documented in this file. Take a look
44

55
**Warning:** Features marked as *experimental* may change or be removed in a future release without notice. Use with caution.
66

7-
<!-- ## [Unreleased] -->
7+
## [Unreleased]
8+
9+
### Fixed
10+
11+
#### Navigator
12+
13+
* Fixed the value of the `scroll` setting when switching from a reflowable EPUB to a fixed-layout one.
14+
15+
#### LCP
16+
17+
* Fixed `IndexOutOfBoundsException` occurring when an LCP-protected EPUB contains incorrect original lengths in its `META-INF/encryption.xml` file.
18+
819

920
## [3.0.1]
1021

readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt

Lines changed: 82 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ internal class LcpDecryptor(
5959
encryption.isDeflated || !encryption.isCbcEncrypted ->
6060
FullLcpResource(resource, encryption, license)
6161
else ->
62-
CbcLcpResource(resource, encryption, license)
62+
CbcLcpResource(resource, license)
6363
}
6464
}
6565
}
@@ -91,7 +91,6 @@ internal class LcpDecryptor(
9191
*/
9292
private class CbcLcpResource(
9393
private val resource: Resource,
94-
private val encryption: Encryption,
9594
private val license: LcpLicense
9695
) : Resource by resource {
9796

@@ -103,14 +102,14 @@ internal class LcpDecryptor(
103102
private lateinit var _length: Try<Long, ReadError>
104103

105104
/*
106-
* Decryption needs to look around the data strictly matching the content to decipher.
107-
* That means that in case of contiguous read requests, data fetched from the underlying
108-
* resource are not contiguous. Every request to the underlying resource starts slightly
109-
* before the end of the previous one. This is an issue with remote publications because
110-
* you have to make a new HTTP request every time instead of reusing the previous one.
111-
* To alleviate this, we cache the three last bytes read in each call and reuse them
112-
* in the next call if possible.
113-
*/
105+
* Decryption needs to look around the data strictly matching the content to decipher.
106+
* That means that in case of contiguous read requests, data fetched from the underlying
107+
* resource are not contiguous. Every request to the underlying resource starts slightly
108+
* before the end of the previous one. This is an issue with remote publications because
109+
* you have to make a new HTTP request every time instead of reusing the previous one.
110+
* To alleviate this, we cache the three last bytes read in each call and reuse them
111+
* in the next call if possible.
112+
*/
114113
private val _cache: Cache = Cache()
115114

116115
/** Plain text size. */
@@ -119,8 +118,8 @@ internal class LcpDecryptor(
119118
return _length
120119
}
121120

122-
_length = encryption.originalLength?.let { Try.success(it) }
123-
?: lengthFromPadding()
121+
// Unfortunately, encryption.originalLength is not reliable.
122+
_length = lengthFromPadding()
124123

125124
return _length
126125
}
@@ -141,7 +140,16 @@ internal class LcpDecryptor(
141140
val bytes = resource.read(readOffset..length)
142141
.getOrElse { return Try.failure(it) }
143142

144-
val decryptedBytes = license.decrypt(bytes)
143+
return lengthFromLastTwoBlocks(length, bytes)
144+
}
145+
146+
private suspend fun lengthFromLastTwoBlocks(
147+
cipheredLength: Long,
148+
lastTwoBlocks: ByteArray
149+
): Try<Long, ReadError> {
150+
require(lastTwoBlocks.size == 2 * AES_BLOCK_SIZE)
151+
152+
val decryptedBytes = license.decrypt(lastTwoBlocks)
145153
.getOrElse {
146154
return Try.failure(
147155
ReadError.Decoding(
@@ -152,11 +160,19 @@ internal class LcpDecryptor(
152160

153161
check(decryptedBytes.size == AES_BLOCK_SIZE)
154162

155-
val adjustedLength = length -
163+
val adjustedLength = cipheredLength -
156164
AES_BLOCK_SIZE - // Minus IV
157165
decryptedBytes.last().toInt() // Minus padding size
158166

159-
return Try.success(adjustedLength)
167+
return if (adjustedLength >= 0) {
168+
Try.success(adjustedLength)
169+
} else {
170+
Try.failure(
171+
ReadError.Decoding(
172+
DebugError("Padding length seems invalid.")
173+
)
174+
)
175+
}
160176
}
161177

162178
override suspend fun read(range: LongRange?): Try<ByteArray, ReadError> {
@@ -173,25 +189,27 @@ internal class LcpDecryptor(
173189
return Try.success(ByteArray(0))
174190
}
175191

192+
val rangeSize = range.last + 1 - range.first
193+
176194
val encryptedLength = resource.length()
177195
.getOrElse { return Try.failure(it) }
178196

197+
// range bounds must be multiple of AES_BLOCK_SIZE and
198+
val startPadding = range.first - range.first.floorMultipleOf(AES_BLOCK_SIZE.toLong())
199+
val endPadding = (range.last + 1).ceilMultipleOf(AES_BLOCK_SIZE.toLong()) - range.last - 1
200+
179201
// encrypted data is shifted by AES_BLOCK_SIZE because of IV and
180202
// the previous block must be provided to perform XOR on intermediate blocks
181-
val encryptedStart = range.first.floorMultipleOf(AES_BLOCK_SIZE.toLong())
182-
val encryptedEndExclusive = (range.last + 1).ceilMultipleOf(AES_BLOCK_SIZE.toLong()) + AES_BLOCK_SIZE
203+
val encryptedStart = range.first - startPadding
204+
val encryptedEndExclusive = range.last + 1 + endPadding + AES_BLOCK_SIZE
183205

184206
val encryptedData = getEncryptedData(encryptedStart until encryptedEndExclusive)
185207
.getOrElse { return Try.failure(it) }
186208

187-
if (encryptedData.size >= _cache.data.size) {
188-
// cache the three last encrypted blocks that have been read for future use
189-
val cacheStart = encryptedData.size - _cache.data.size
190-
_cache.startIndex = (encryptedEndExclusive - _cache.data.size).toInt()
191-
encryptedData.copyInto(_cache.data, 0, cacheStart)
192-
}
193-
194209
val bytes = license.decrypt(encryptedData)
210+
.onSuccess {
211+
check(it.isEmpty() || it.size == encryptedData.size - AES_BLOCK_SIZE)
212+
}
195213
.getOrElse {
196214
return Try.failure(
197215
ReadError.Decoding(
@@ -203,29 +221,49 @@ internal class LcpDecryptor(
203221
)
204222
}
205223

206-
// exclude the bytes added to match a multiple of AES_BLOCK_SIZE
207-
val sliceStart = (range.first - encryptedStart).toInt()
208-
209224
// was the last block read to provide the desired range
210225
val lastBlockRead = encryptedLength - encryptedEndExclusive <= AES_BLOCK_SIZE
211226

212-
val rangeLength =
227+
val dataSlice =
213228
if (lastBlockRead) {
229+
val decryptedLength =
230+
if (::_length.isInitialized) {
231+
_length
232+
} else {
233+
val lastTwoBlocks = encryptedData.sliceArray(
234+
encryptedData.size - 2 until encryptedData.size
235+
)
236+
lengthFromLastTwoBlocks(encryptedLength, lastTwoBlocks)
237+
.onSuccess { _length = Try.success(it) }
238+
}.getOrElse { return Try.failure(it) }
239+
214240
// use decrypted length to ensure range.last doesn't exceed decrypted length - 1
215-
val decryptedLength = length()
216-
.getOrElse { return Try.failure(it) }
217-
range.last.coerceAtMost(decryptedLength - 1) - range.first + 1
241+
val dataLength = (range.last + 1).coerceAtMost(decryptedLength) - range.first
242+
243+
// keep only enough bytes to fit the length corrected request in order to never include padding
244+
val sliceEnd = startPadding + dataLength.toInt()
245+
246+
startPadding.toInt() until sliceEnd.toInt()
218247
} else {
219-
// the last block won't be read, so there's no need to compute length
220-
range.last - range.first + 1
248+
// the last block was not read, so there's no need to compute decrypted length
249+
250+
// bytes contains deciphered data for startPadding, then for the requested
251+
// range, and then for endPadding
252+
// the requested range might have been far too large, in which case bytes doesn't
253+
// content all of that data
254+
// if there are any data for endPadding, it begins at endPaddingStartIndex.
255+
val endPaddingStartIndex = (startPadding + rangeSize).coerceAtMost(
256+
bytes.size.toLong()
257+
)
258+
startPadding.toInt() until endPaddingStartIndex.toInt()
221259
}
222260

223-
// keep only enough bytes to fit the length corrected request in order to never include padding
224-
val sliceEnd = sliceStart + rangeLength.toInt()
225-
226-
return Try.success(bytes.sliceArray(sliceStart until sliceEnd))
261+
return Try.success(bytes.sliceArray(dataSlice))
227262
}
228263

264+
/**
265+
* Reads encrypted data using the cache when suitable.
266+
*/
229267
private suspend fun getEncryptedData(range: LongRange): Try<ByteArray, ReadError> {
230268
val cacheStartIndex = _cache.startIndex
231269
?.takeIf { cacheStart ->
@@ -241,6 +279,13 @@ internal class LcpDecryptor(
241279
_cache.data.copyInto(bytes, 0, offsetInCache)
242280
it.copyInto(bytes, fromCacheLength)
243281
bytes
282+
}.onSuccess { result ->
283+
if (result.size >= _cache.data.size) {
284+
// cache the three last encrypted blocks that have been read for future use
285+
val cacheStart = result.size - _cache.data.size
286+
_cache.startIndex = (range.last + 1 - _cache.data.size).toInt()
287+
result.copyInto(_cache.data, 0, cacheStart)
288+
}
244289
}
245290
}
246291

readium/shared/src/main/java/org/readium/r2/shared/util/io/CountingInputStream.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ public class CountingInputStream(
7171
.coerceFirstNonNegative()
7272
.requireLengthFitInt()
7373

74+
require(range.first >= count)
75+
7476
if (range.isEmpty()) {
7577
return ByteArray(0)
7678
}
@@ -81,7 +83,12 @@ public class CountingInputStream(
8183
while (skipped != toSkip) {
8284
skipped += skip(toSkip - skipped)
8385
if (skipped == 0L) {
84-
throw IOException("Could not skip InputStream to read ranges.")
86+
if (read() == -1) {
87+
// End reached, range.first was greater or equal to content length
88+
return ByteArray(0)
89+
} else {
90+
throw IOException("Could not skip InputStream to read ranges.")
91+
}
8592
}
8693
}
8794

0 commit comments

Comments
 (0)