This repository has been archived by the owner on Aug 12, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for byte range requests for DRM protected resources
- Loading branch information
1 parent
2ce2878
commit 6f95ad4
Showing
11 changed files
with
426 additions
and
115 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
// | ||
// CBCDRMInputStream.swift | ||
// r2-streamer-swift | ||
// | ||
// Created by Mickaël Menu on 04.07.19. | ||
// | ||
// Copyright 2019 Readium Foundation. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license which is detailed | ||
// in the LICENSE file present in the project repository where this source code is maintained. | ||
// | ||
|
||
import Foundation | ||
|
||
|
||
private let AESBlockSize: Int64 = 16 // bytes | ||
|
||
/// A DRM input stream to read content encrypted with the CBC algorithm. Support random access for byte range requests. | ||
final class CBCDRMInputStream: DRMInputStream { | ||
|
||
enum Error: Swift.Error { | ||
case invalidStream | ||
case emptyDecryptedData | ||
case readOutOfRange | ||
case readEncryptedOutOfRange | ||
case readFailed | ||
case decryptionFailed | ||
} | ||
|
||
private lazy var plainTextSize: Int64 = { | ||
guard stream.length >= 2 * AESBlockSize else { | ||
fail(with: Error.invalidStream) | ||
return 0 | ||
} | ||
|
||
do { | ||
let readPosition = Int64(stream.length) - 2 * AESBlockSize | ||
let bufferSize = 2 * AESBlockSize | ||
var buffer = Array<UInt8>(repeating: 0, count: Int(bufferSize)) | ||
|
||
stream.open() | ||
try stream.seek(offset: readPosition, whence: .startOfFile) | ||
let numberOfBytesRead = stream.read(&buffer, maxLength: Int(bufferSize)) | ||
let data = Data(bytes: buffer, count: numberOfBytesRead) | ||
stream.close() | ||
|
||
guard let decryptedData = try license.decipher(data) else { | ||
fail(with: Error.emptyDecryptedData) | ||
return 0 | ||
} | ||
|
||
return Int64(stream.length) | ||
- AESBlockSize // Minus IV or previous block | ||
- (AESBlockSize - Int64(decryptedData.count)) % AESBlockSize // Minus padding part | ||
|
||
} catch { | ||
fail(with: error) | ||
return 0 | ||
} | ||
}() | ||
|
||
override var length: UInt64 { | ||
return UInt64(plainTextSize) | ||
} | ||
|
||
override func read(_ buffer: UnsafeMutablePointer<UInt8>, maxLength len: Int) -> Int { | ||
guard hasBytesAvailable else { | ||
return 0 | ||
} | ||
|
||
let len = Int64(len) | ||
let offset = Int64(self.offset) | ||
guard offset + len <= length else { | ||
fail(with: Error.readOutOfRange) | ||
return -1 | ||
} | ||
|
||
// Get offset result offset in the block. | ||
let blockOffset = offset % AESBlockSize | ||
// For beginning of the cipher text, IV used for XOR. | ||
// For cipher text in the middle, previous block used for XOR. | ||
let readPosition = offset - blockOffset | ||
|
||
// Count blocks to read. | ||
// First block for IV or previous block to perform XOR. | ||
var blocksCount: Int64 = 1 | ||
var bytesInFirstBlock: Int64 = (AESBlockSize - blockOffset) % AESBlockSize | ||
if len < bytesInFirstBlock { | ||
bytesInFirstBlock = 0 | ||
} | ||
if bytesInFirstBlock > 0 { | ||
blocksCount += 1 | ||
} | ||
|
||
blocksCount += (len - bytesInFirstBlock) / AESBlockSize | ||
if (len - bytesInFirstBlock) % AESBlockSize != 0 { | ||
blocksCount += 1 | ||
} | ||
|
||
// Read data from the stream | ||
var data: Data | ||
do { | ||
let bufferSize = blocksCount * AESBlockSize | ||
var buffer = Array<UInt8>(repeating: 0, count: Int(bufferSize)) | ||
guard readPosition + bufferSize <= stream.length else { | ||
fail(with: Error.readEncryptedOutOfRange) | ||
return -1 | ||
} | ||
|
||
stream.open() | ||
try stream.seek(offset: Int64(readPosition), whence: .startOfFile) | ||
let numberOfBytesRead = stream.read(&buffer, maxLength: Int(bufferSize)) | ||
assert(numberOfBytesRead >= 0) | ||
data = Data(bytes: buffer, count: numberOfBytesRead) | ||
stream.close() | ||
|
||
} catch { | ||
fail(with: Error.readFailed) | ||
return -1 | ||
} | ||
|
||
do { | ||
guard let decryptedData = try license.decipher(data) else { | ||
fail(with: Error.emptyDecryptedData) | ||
return -1 | ||
} | ||
data = decryptedData | ||
} catch { | ||
fail(with: Error.decryptionFailed) | ||
return -1 | ||
} | ||
|
||
if data.count > len { | ||
data = data[0..<min(data.count, Int(len))] | ||
} | ||
data.copyBytes(to: buffer, count: data.count) | ||
_offset += UInt64(data.count) | ||
if _offset >= length { | ||
_streamStatus = .atEnd | ||
} | ||
return data.count | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// | ||
// DRMDecoder.swift | ||
// r2-streamer-swift | ||
// | ||
// Created by Alexandre Camilleri on 10/11/17. | ||
// | ||
// Copyright 2018 Readium Foundation. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license which is detailed | ||
// in the LICENSE file present in the project repository where this source code is maintained. | ||
// | ||
|
||
import Foundation | ||
import R2Shared | ||
|
||
|
||
/// Decrypt DRM encrypted content. | ||
class DRMDecoder: Loggable { | ||
|
||
/// Decode the given stream using DRM. If it fails, just return the | ||
/// stream unchanged. | ||
/// | ||
/// - Parameters: | ||
/// - input: The input stream. | ||
/// - resourceLink: The link represented by the stream. | ||
/// - drm: The DRM object used for decryption. | ||
/// - Returns: The decrypted stream. | ||
static func decoding(_ input: SeekableInputStream, of resourceLink: Link, with drm: DRM?) -> SeekableInputStream { | ||
/// Check if the resource is encrypted. | ||
guard let drm = drm, | ||
let license = drm.license, | ||
let encryption = resourceLink.properties.encryption, | ||
let originalLength = encryption.originalLength, | ||
let scheme = encryption.scheme, | ||
// Check that the encryption schemes of ressource and DRM are the same. | ||
scheme == drm.scheme.rawValue else | ||
{ | ||
return input | ||
} | ||
|
||
let isDeflated = (encryption.compression == "deflate") | ||
let isCBC = (encryption.algorithm == "http://www.w3.org/2001/04/xmlenc#aes256-cbc") | ||
return (isDeflated || !isCBC) | ||
? FullDRMInputStream(stream: input, link: resourceLink, license: license, originalLength: originalLength, isDeflated: isDeflated) | ||
: CBCDRMInputStream(stream: input, link: resourceLink, license: license, originalLength: originalLength) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
// | ||
// DRMInputStream.swift | ||
// r2-streamer-swift | ||
// | ||
// Created by Mickaël Menu on 04.07.19. | ||
// | ||
// Copyright 2019 Readium Foundation. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license which is detailed | ||
// in the LICENSE file present in the project repository where this source code is maintained. | ||
// | ||
|
||
import Foundation | ||
import R2Shared | ||
|
||
|
||
class DRMInputStream: SeekableInputStream, Loggable { | ||
|
||
let stream: SeekableInputStream | ||
let link: Link | ||
let license: DRMLicense | ||
let originalLength: Int | ||
|
||
init(stream: SeekableInputStream, link: Link, license: DRMLicense, originalLength: Int) { | ||
self.stream = stream | ||
self.link = link | ||
self.license = license | ||
self.originalLength = originalLength | ||
super.init() | ||
} | ||
|
||
|
||
// MARK: - Seekable | ||
|
||
override var length: UInt64 { | ||
return UInt64(originalLength) | ||
} | ||
|
||
var _offset: UInt64 = 0 | ||
override var offset: UInt64 { return _offset } | ||
|
||
override func seek(offset: Int64, whence: SeekWhence) throws { | ||
let length = Int64(self.length) | ||
switch whence { | ||
case .startOfFile: | ||
assert(0...length ~= offset) | ||
_offset = UInt64(offset) | ||
case .endOfFile: | ||
assert(-length...0 ~= offset) | ||
_offset = UInt64(length + offset) | ||
case .currentPosition: | ||
let newOffset = Int64(_offset) + offset | ||
assert(0...length ~= newOffset) | ||
_offset = UInt64(offset) | ||
} | ||
} | ||
|
||
|
||
// MARK: - Stream | ||
|
||
var _streamStatus: Stream.Status = .notOpen | ||
override var streamStatus: Stream.Status { return _streamStatus } | ||
|
||
private var _streamError: Error? | ||
override var streamError: Error? { return _streamError } | ||
|
||
func fail(with error: Error) { | ||
_streamStatus = .error | ||
_streamError = error | ||
log(.error, "\(type(of: self)): \(link.href): \(error.localizedDescription)") | ||
} | ||
|
||
override func open() { | ||
_streamStatus = .open | ||
} | ||
|
||
override func close() { | ||
_offset = 0 | ||
_streamStatus = .notOpen | ||
} | ||
|
||
// MARK: - InputStream | ||
|
||
override var hasBytesAvailable: Bool { | ||
return offset < length | ||
} | ||
|
||
override func getBuffer(_ buffer: UnsafeMutablePointer<UnsafeMutablePointer<UInt8>?>, length len: UnsafeMutablePointer<Int>) -> Bool { | ||
return false | ||
} | ||
|
||
} |
Oops, something went wrong.