/
AnimeTwist+Episode.swift
143 lines (127 loc) · 6.11 KB
/
AnimeTwist+Episode.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//
// This file is part of the NineAnimator project.
//
// Copyright © 2018-2019 Marcus Zhou. All rights reserved.
//
// NineAnimator is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// NineAnimator is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with NineAnimator. If not, see <http://www.gnu.org/licenses/>.
//
import CommonCrypto
import Foundation
extension NASourceAnimeTwist {
fileprivate static let encryptionKey = "LXgIVP&PorO68Rq7dTx8N^lP!Fa5sGJ^*XK".data(using: .utf8)!
func episode(from link: EpisodeLink, with anime: Anime) -> NineAnimatorPromise<Episode> {
guard let sourcePool = anime.additionalAttributes["twist.source"] as? [EpisodeLink: String],
let encryptedSourceString = sourcePool[link],
let encryptedSource = Data(base64Encoded: encryptedSourceString) else {
return .fail(NineAnimatorError.providerError("Streaming source cannot be found"))
}
// Unlike other sources, anime on twist.moe are self-hosted and encrypted.
// So the major portion of the code here is to decrypt the source and assign
// it to the targetUrl property.
return NineAnimatorPromise.firstly {
// Check if the data has the prefix
let saltIdentifier = "Salted__"
guard String(data: encryptedSource[0..<8], encoding: .utf8) == saltIdentifier else {
throw NineAnimatorError.responseError("Invalid source")
}
// 8 bytes of salt
let salt = encryptedSource[8..<16]
let data = encryptedSource[16...]
// Calculate the key and iv
let (key, iv) = self.generateKeyAndIV(NASourceAnimeTwist.encryptionKey, salt: salt)
let destinationBufferLength = data.count + kCCBlockSizeAES128
var destinationBuffer = Data(count: destinationBufferLength)
var decryptedBytes = 0
// AES256-CBC decrypt with the derived key and iv
let decryptionStatus = destinationBuffer.withUnsafeMutableBytes {
destinationPointer in
data.withUnsafeBytes {
dataPointer in
key.withUnsafeBytes {
keyPointer in
iv.withUnsafeBytes {
ivPointer in
CCCrypt(
CCOperation(kCCDecrypt),
CCAlgorithm(kCCAlgorithmAES),
CCOptions(kCCOptionPKCS7Padding),
keyPointer.baseAddress!,
kCCKeySizeAES256,
ivPointer.baseAddress!,
dataPointer.baseAddress!,
data.count,
destinationPointer.baseAddress!,
destinationBufferLength,
&decryptedBytes
)
}
}
}
}
// Check result status
guard decryptionStatus == CCCryptorStatus(kCCSuccess) else {
throw NineAnimatorError.responseError("Unable to decrypt video streaming information")
}
// Convert decrypted path to string
guard let episodePath = String(data: destinationBuffer[0..<decryptedBytes], encoding: .utf8) else {
throw NineAnimatorError.responseError("Video streaming information is corrupted")
}
// Construct episode target url
let episodeUrl = self.endpointURL.appendingPathComponent(episodePath)
Log.info("[twist.moe] Decrypted video URL at %@", episodeUrl.absoluteString)
// Construt Episode struct
return Episode(link, target: episodeUrl, parent: anime)
}
}
/// Derives the 32-byte AES key and the 16-byte IV from data and salt
///
/// See OpenSSL's implementation of EVP_BytesToKey
private func generateKeyAndIV(_ data: Data, salt: Data) -> (key: Data, iv: Data) {
let totalLength = 48
var destinationBuffer = Data(capacity: totalLength)
let dataAndSalt = data + salt
// Calculate the key and value with data and salt
var digestBuffer = Data(count: Int(CC_MD5_DIGEST_LENGTH))
_ = digestBuffer.withUnsafeMutableBytes {
destinationPointer in
dataAndSalt.withUnsafeBytes {
(pointer: UnsafeRawBufferPointer) in
CC_MD5(
pointer.baseAddress!,
CC_LONG(dataAndSalt.count),
destinationPointer.bindMemory(to: UInt8.self).baseAddress!
)
}
}
destinationBuffer.append(digestBuffer)
// Keep digesting until the buffer is filled
while destinationBuffer.count < totalLength {
let combined = digestBuffer + dataAndSalt
_ = digestBuffer.withUnsafeMutableBytes {
(destinationPointer: UnsafeMutableRawBufferPointer) in
combined.withUnsafeBytes {
pointer in
CC_MD5(
pointer.baseAddress!,
CC_LONG(combined.count),
destinationPointer.bindMemory(to: UInt8.self).baseAddress!
)
}
}
destinationBuffer.append(digestBuffer)
}
// Generate key and iv
return (destinationBuffer[0..<32], destinationBuffer[32..<48])
}
}