Skip to content

Commit

Permalink
Implement agile decryption
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxDesiatov committed May 29, 2020
1 parent d303769 commit 31bd9b6
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 30 deletions.
11 changes: 10 additions & 1 deletion Package.resolved
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
{
"object": {
"pins": [
{
"package": "CryptoSwift",
"repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git",
"state": {
"branch": null,
"revision": "39f08ac5269361a50c08ce1e2f41989bfc4b1ec8",
"version": "1.3.1"
}
},
{
"package": "OLEKit",
"repositoryURL": "https://github.com/CoreOffice/OLEKit.git",
"state": {
"branch": "master",
"revision": "c398a516de75f6b974488bae6bbe89a1ca09caf1",
"revision": "27ffa93dc27e5e857350552adef7ef22ae208548",
"version": null
}
},
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ let package = Package(
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/apple/swift-crypto.git", from: "1.0.1"),
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.3.1"),
.package(url: "https://github.com/MaxDesiatov/XMLCoder.git", .upToNextMinor(from: "0.11.1")),
.package(url: "https://github.com/CoreOffice/OLEKit.git", .branch("master")),
.package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMinor(from: "0.9.11")),
Expand All @@ -33,7 +34,7 @@ let package = Package(
// a module or a test suite.
// Targets can depend on other targets in this package, and on products in
// packages which this package depends on.
.target(name: "CryptoOffice", dependencies: ["Crypto", "OLEKit", "XMLCoder"]),
.target(name: "CryptoOffice", dependencies: ["Crypto", "CryptoSwift", "OLEKit", "XMLCoder"]),
.testTarget(name: "CryptoOfficeTests", dependencies: ["CryptoOffice", "ZIPFoundation"]),
]
)
53 changes: 26 additions & 27 deletions Sources/CryptoOffice/CryptoOffice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,59 +12,58 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import Crypto
import Foundation
import OLEKit

struct AgileInfo: Decodable {
struct KeyData: Decodable {
let saltValue: Data
let hashAlgorihm: String
}

struct Password: Decodable {
let spinCount: Int
let encryptedKeyValue: Data
let saltValue: Data
let hashAlgorihm: String
let keyBits: Int
}

let keyData: KeyData
let password: Password
}

public enum EncryptionType {
case agile
}
import XMLCoder

public final class CryptoOfficeFile {
private let oleFile: OLEFile

public let encryptionType: EncryptionType
let encryptionInfo: AgileInfo

let packageEntry: DirectoryEntry

public init(path: String) throws {
do {
oleFile = try OLEFile(path)
guard let entry = oleFile.root.children.first(where: { $0.name == "EncryptionInfo" })
guard
let infoEntry = oleFile.root.children.first(where: { $0.name == "EncryptionInfo" }),
let packageEntry = infoEntry.children.first(where: { $0.name == "EncryptedPackage" })
else { throw CryptoOfficeError.fileIsNotEncrypted(path: path) }

let stream = try oleFile.stream(entry)
self.packageEntry = packageEntry
let stream = try oleFile.stream(infoEntry)

let major: UInt16 = stream.read()
let minor: UInt16 = stream.read()
switch (major, minor) {
case (4, 4):
encryptionType = .agile
let decoder = XMLDecoder()
decoder.shouldProcessNamespaces = true

stream.seek(toOffset: 8)
let info = try decoder.decode(AgileInfo.self, from: stream.readDataToEnd())
encryptionInfo = info

case (2, 2), (3, 2), (4, 2):
throw CryptoOfficeError.standardEncryptionNotSupported

case (3, 3), (4, 3):
throw CryptoOfficeError.extensibleEncryptionNotSupported

default:
throw CryptoOfficeError.unknownEncryptionVersion(major: major, minor: minor)
}
} catch let OLEError.fileIsNotOLE(path) {
throw CryptoOfficeError.fileIsNotEncrypted(path: path)
}
}

public func decrypt(password: String) throws -> Data {
let secretKey = try encryptionInfo.secretKey(password: password)

let stream = try oleFile.stream(packageEntry)

return try encryptionInfo.decrypt(stream, secretKey: secretKey)
}
}
3 changes: 3 additions & 0 deletions Sources/CryptoOffice/CryptoOfficeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
public enum CryptoOfficeError: Error {
case standardEncryptionNotSupported
case fileIsNotEncrypted(path: String)
case cantEncodePassword(encoding: String.Encoding)
case extensibleEncryptionNotSupported
case encryptedKeyNotSpecifiedForAgileEncryption
case unknownEncryptionVersion(major: UInt16, minor: UInt16)
case hashAlgorithmNotSupported(actual: String?, expected: [String])
}
117 changes: 117 additions & 0 deletions Sources/CryptoOffice/ECMA376Agile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2020 CoreOffice contributors
//
// 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.

import Crypto
import CryptoSwift
import Foundation
import OLEKit

extension Crypto.Digest {
var data: Data { Data(makeIterator()) }
}

struct AgileInfo: Decodable {
struct KeyData: Decodable {
let saltValue: Data
let hashAlgorithm: String
}

struct EncryptedKey: Decodable {
let spinCount: UInt32
let encryptedKeyValue: Data
let saltValue: Data
let hashAlgorithm: String
let keyBits: Int
}

struct KeyEncryptors: Decodable {
struct KeyEncryptor: Decodable {
let encryptedKey: EncryptedKey
}

let keyEncryptor: [KeyEncryptor]
}

let keyData: KeyData
let keyEncryptors: KeyEncryptors

func secretKey(password: String) throws -> [UInt8] {
let block3 = Data([0x14, 0x6E, 0x0B, 0xE7, 0xAB, 0xAC, 0xD0, 0xD6])

guard let encryptedKey = keyEncryptors.keyEncryptor.first?.encryptedKey else {
throw CryptoOfficeError.encryptedKeyNotSpecifiedForAgileEncryption
}

let algorithm = encryptedKey.hashAlgorithm.lowercased()

guard algorithm == "sha512" else {
throw CryptoOfficeError.hashAlgorithmNotSupported(actual: algorithm, expected: ["sha512"])
}
guard let passwordData = password.data(using: .utf16LittleEndian)
else { throw CryptoOfficeError.cantEncodePassword(encoding: .utf16LittleEndian) }

// Initial round sha512(salt + password)
var hash = SHA512.hash(data: encryptedKey.saltValue + passwordData)

// Iteration of 0 -> spincount-1; hash = sha512(iterator + hash)
for i in 0..<encryptedKey.spinCount {
let spin = DataWriter()
spin.write(i)
hash = SHA512.hash(data: spin.data + hash.data)
}

hash = SHA512.hash(data: hash.data + block3)

// truncate to bitsize
let decryptionKey = hash.data[0..<(encryptedKey.keyBits / 8)].bytes

let aes = try CryptoSwift.AES(
key: decryptionKey,
blockMode: CBC(iv: encryptedKey.saltValue.bytes),
padding: .noPadding
)
let result = try aes.decrypt(encryptedKey.encryptedKeyValue.bytes)
return result
}

func decrypt(_ reader: DataReader, secretKey: [UInt8]) throws -> Data {
let segmentLength: UInt32 = 4096

let totalSize: UInt32 = reader.read()
let lastSegmentSize = totalSize % segmentLength

reader.seek(toOffset: 8)

var result = Data()
let totalSegments = totalSize / segmentLength + (lastSegmentSize > 0 ? 1 : 0)

for i in 0..<totalSegments {
let segmentIndex = DataWriter()
segmentIndex.write(i)

let iv = (keyData.saltValue + segmentIndex.data).sha512()[0..<16].bytes
let aes = try AES(key: secretKey, blockMode: CBC(iv: iv))

let chunk: Data
if i == totalSegments - 1 {
chunk = reader.readDataToEnd()
} else {
chunk = reader.readData(ofLength: Int(segmentLength))
}
try result.append(Data(aes.decrypt(chunk.bytes)))
}

return result
}
}
10 changes: 9 additions & 1 deletion Tests/CryptoOfficeTests/CryptoOfficeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

@testable import CryptoOffice
import XCTest
import ZIPFoundation

final class CryptoOfficeTests: XCTestCase {
func testWorkbook() throws {
Expand All @@ -22,6 +23,13 @@ final class CryptoOfficeTests: XCTestCase {
.appendingPathComponent("TestWorkbook.xlsx")

let file = try CryptoOfficeFile(path: url.path)
XCTAssertEqual(file.encryptionType, .agile)
XCTAssertEqual(
file.encryptionInfo.keyEncryptors.keyEncryptor[0].encryptedKey.hashAlgorithm,
"SHA512"
)

let data = try file.decrypt(password: "pass")
let archive = try XCTUnwrap(Archive(data: data, accessMode: .read))
XCTAssertEqual(Array(archive).count, 10)
}
}

0 comments on commit 31bd9b6

Please sign in to comment.