Skip to content
This repository has been archived by the owner on Aug 12, 2022. It is now read-only.

Commit

Permalink
Add support for byte range requests for DRM protected resources
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-menu-mantano committed Jul 4, 2019
1 parent 2ce2878 commit 6f95ad4
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 115 deletions.
22 changes: 21 additions & 1 deletion r2-streamer-swift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
CA6F97D222A5161A007D2049 /* EPUBContainerParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6F97D122A5161A007D2049 /* EPUBContainerParserTests.swift */; };
CA6F97D422A52810007D2049 /* OPFParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6F97D322A52810007D2049 /* OPFParserTests.swift */; };
CA6F97DA22A6A0B7007D2049 /* OPFMeta.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6F97D922A6A0B6007D2049 /* OPFMeta.swift */; };
CABAC16622CDD6A200360595 /* DRMInputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABAC16522CDD6A200360595 /* DRMInputStream.swift */; };
CABAC16822CDD7A500360595 /* FullDRMInputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABAC16722CDD7A500360595 /* FullDRMInputStream.swift */; };
CABAC16A22CDD83C00360595 /* CBCDRMInputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABAC16922CDD83C00360595 /* CBCDRMInputStream.swift */; };
CAF58199229EC8B3009A04E8 /* EPUBMetadataParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF58198229EC8B3009A04E8 /* EPUBMetadataParserTests.swift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -132,6 +135,9 @@
CA6F97D122A5161A007D2049 /* EPUBContainerParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EPUBContainerParserTests.swift; sourceTree = "<group>"; };
CA6F97D322A52810007D2049 /* OPFParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPFParserTests.swift; sourceTree = "<group>"; };
CA6F97D922A6A0B6007D2049 /* OPFMeta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPFMeta.swift; sourceTree = "<group>"; };
CABAC16522CDD6A200360595 /* DRMInputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DRMInputStream.swift; sourceTree = "<group>"; };
CABAC16722CDD7A500360595 /* FullDRMInputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullDRMInputStream.swift; sourceTree = "<group>"; };
CABAC16922CDD83C00360595 /* CBCDRMInputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CBCDRMInputStream.swift; sourceTree = "<group>"; };
CAF58198229EC8B3009A04E8 /* EPUBMetadataParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EPUBMetadataParserTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -225,7 +231,7 @@
isa = PBXGroup;
children = (
CA130D1C229D4245000A627C /* FontDecoder.swift */,
CA130D1D229D4245000A627C /* DRMDecoder.swift */,
CABAC16422CDD67A00360595 /* DRM */,
CA130D1E229D4245000A627C /* ContentFilter.swift */,
CA130D1F229D4245000A627C /* Fetcher.swift */,
CA130D20229D4245000A627C /* DataCompression.swift */,
Expand Down Expand Up @@ -372,6 +378,17 @@
path = EPUB;
sourceTree = "<group>";
};
CABAC16422CDD67A00360595 /* DRM */ = {
isa = PBXGroup;
children = (
CA130D1D229D4245000A627C /* DRMDecoder.swift */,
CABAC16522CDD6A200360595 /* DRMInputStream.swift */,
CABAC16722CDD7A500360595 /* FullDRMInputStream.swift */,
CABAC16922CDD83C00360595 /* CBCDRMInputStream.swift */,
);
path = DRM;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXHeadersBuildPhase section */
Expand Down Expand Up @@ -500,11 +517,14 @@
CA130D6B229D4245000A627C /* EPUBEncryptionParser.swift in Sources */,
CA130D64229D4245000A627C /* PDFFileParser.swift in Sources */,
CA130D44229D4245000A627C /* Seekable.swift in Sources */,
CABAC16822CDD7A500360595 /* FullDRMInputStream.swift in Sources */,
CA130D6F229D4245000A627C /* FileContainer.swift in Sources */,
CA130D5B229D4245000A627C /* DataCompression.swift in Sources */,
CA6F97DA22A6A0B7007D2049 /* OPFMeta.swift in Sources */,
CA130D74229D4245000A627C /* DataExtension.swift in Sources */,
CABAC16622CDD6A200360595 /* DRMInputStream.swift in Sources */,
CA130D75229D4245000A627C /* StringExtension.swift in Sources */,
CABAC16A22CDD83C00360595 /* CBCDRMInputStream.swift in Sources */,
CA130D68229D4245000A627C /* NCXParser.swift in Sources */,
CA130D6A229D4245000A627C /* NavigationDocumentParser.swift in Sources */,
CA130D5A229D4245000A627C /* Fetcher.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion r2-streamer-swift/Fetcher/ContentFilter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ final internal class ContentFiltersEpub: ContentFilters {
guard let resourceLink = publication.link(withHref: path) else {
return input
}
var decodedInputStream = DrmDecoder.decoding(input, of: resourceLink, with: container.drm)
var decodedInputStream = DRMDecoder.decoding(input, of: resourceLink, with: container.drm)
decodedInputStream = FontDecoder.decoding(decodedInputStream,
of: resourceLink,
publication.metadata.identifier)
Expand Down
143 changes: 143 additions & 0 deletions r2-streamer-swift/Fetcher/DRM/CBCDRMInputStream.swift
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
}

}
47 changes: 47 additions & 0 deletions r2-streamer-swift/Fetcher/DRM/DRMDecoder.swift
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)
}

}
91 changes: 91 additions & 0 deletions r2-streamer-swift/Fetcher/DRM/DRMInputStream.swift
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
}

}
Loading

0 comments on commit 6f95ad4

Please sign in to comment.