Skip to content

Commit

Permalink
Obtain receipt
Browse files Browse the repository at this point in the history
  • Loading branch information
Carlos Cabanero committed Dec 15, 2021
1 parent d912618 commit 8f11c15
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 50 deletions.
4 changes: 4 additions & 0 deletions Blink.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
BDBFA3222728925C00C77798 /* FileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBFA3202728925C00C77798 /* FileSystem.swift */; };
BDBFA3232728927000C77798 /* BlinkFiles.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 07FABBAF25C9AECF00E1CC2C /* BlinkFiles.framework */; };
BDBFA3282728927E00C77798 /* BlinkFiles.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 07FABBAF25C9AECF00E1CC2C /* BlinkFiles.framework */; };
BDB72137274821C000EE819A /* Receipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB72136274821C000EE819A /* Receipt.swift */; };
BDCB7165268E1577007D7047 /* BlinkConfig.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BDCB715E268E1577007D7047 /* BlinkConfig.framework */; };
BDCB7166268E1577007D7047 /* BlinkConfig.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BDCB715E268E1577007D7047 /* BlinkConfig.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BDCB7178268E15A2007D7047 /* BKHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCB716B268E15A0007D7047 /* BKHosts.swift */; };
Expand Down Expand Up @@ -784,6 +785,7 @@
BD9EA20A271F62ED00874007 /* BlinkLogging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlinkLogging.swift; sourceTree = "<group>"; };
BD9EA20C271F664D00874007 /* Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publisher.swift; sourceTree = "<group>"; };
BD9EA215271F83B400874007 /* BlinkLoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlinkLoggingTests.swift; sourceTree = "<group>"; };
BDB72136274821C000EE819A /* Receipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Receipt.swift; sourceTree = "<group>"; };
BDB8BEA726E008190093BF48 /* OwnAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnAlertController.swift; sourceTree = "<group>"; };
BDBFA3052728914F00C77798 /* BlinkCode.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BlinkCode.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BDBFA3072728914F00C77798 /* BlinkCode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BlinkCode.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1238,6 +1240,7 @@
D275492C26A033CC0039CC95 /* FaceCam.swift */,
BD1758AB26EA8C5400AEC545 /* MenuController.swift */,
D2A80978270713D200CD0FAF /* FeatureFlags.swift */,
BDB72136274821C000EE819A /* Receipt.swift */,
);
path = Blink;
sourceTree = "<group>";
Expand Down Expand Up @@ -2998,6 +3001,7 @@
D2A80979270713D200CD0FAF /* FeatureFlags.swift in Sources */,
D2C2441A238E44AB0082C69C /* KeySection.swift in Sources */,
079635871D0E6602000473B1 /* TermView.m in Sources */,
BDB72137274821C000EE819A /* Receipt.swift in Sources */,
D241CBE6230562E9003D64A5 /* KBSizes.swift in Sources */,
D241CBD023040734003D64A5 /* KBKeyViewFlexible.swift in Sources */,
);
Expand Down
68 changes: 18 additions & 50 deletions Blink/Commands/skstore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,71 +53,39 @@ struct SKStoreCmd: NonStdIOCommand {
help: "attribute"
)
var attribute: String
var infoURL: URL { BlinkPaths.blinkURL().appendingPathComponent(".receiptInfo") }

func run() throws {
let sema = DispatchSemaphore(value: 0)

if attribute != "" {
if attribute != "asdf" {
return
}

let sk = SKStore()
sk.start { message in
if let message = message {
print(message)
var error: Error? = nil

let c = sk.fetchReceiptURLPublisher()
.tryMap { url in
let d = try Data(contentsOf: url, options: .alwaysMapped)
let str = d.base64EncodedString(options: [])
try str.write(to: infoURL, atomically: false, encoding: .utf8)
}
sema.signal()
}
sema.wait()
}
}
.sink(receiveCompletion: { completion in
if case .failure(let err) = completion {
error = err
}
sema.signal()
}, receiveValue: { _ in })

@objc class SKStore: NSObject {
var infoURL: URL { BlinkPaths.blinkURL().appendingPathComponent(".receiptInfo") }
var done: ((String?) -> Void)!
var skReq: SKReceiptRefreshRequest? = nil

@objc func start(done: @escaping ((String?) -> Void)) {
self.done = done
guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else {
return done("No URL for receipt found.")
}
if !FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
let skReq = SKReceiptRefreshRequest(receiptProperties: nil)
skReq.delegate = self
skReq.start()
self.skReq = skReq
return
}
sema.wait()

store(appStoreReceiptURL)
}

func store(_ url: URL) {
do {
let d = try Data(contentsOf: url, options: .alwaysMapped)
let str = d.base64EncodedString(options: [])
try str.write(to: infoURL, atomically: false, encoding: .utf8)
done(nil)
} catch {
done(error.localizedDescription)
if let error = error {
throw error
}
}
}

extension SKStore: SKRequestDelegate {
func requestDidFinish(_ request: SKRequest) {
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
store(appStoreReceiptURL)
} else {
done("No receipt found after request.")
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
done(error.localizedDescription)
}
}

@_cdecl("skstore_main")
public func skstore_main(argc: Int32, argv: Argv) -> Int32 {
Expand Down
190 changes: 190 additions & 0 deletions Blink/Receipt.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
//////////////////////////////////////////////////////////////////////////////////
//
// B L I N K
//
// Copyright (C) 2016-2019 Blink Mobile Shell Project
//
// This file is part of Blink.
//
// Blink 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.
//
// Blink 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 Blink. If not, see <http://www.gnu.org/licenses/>.
//
// In addition, Blink is also subject to certain additional terms under
// GNU GPL version 3 section 7.
//
// You should have received a copy of these additional terms immediately
// following the terms and conditions of the GNU General Public License
// which accompanied the Blink Source Code. If not, see
// <http://www.github.com/blinksh/blink>.
//
////////////////////////////////////////////////////////////////////////////////


import Combine
import CryptoKit
import Foundation
import StoreKit


fileprivate let endpointURL = URL("https://us-central1-gold-stone-332203.cloudfunctions.net/receiptEntitlementPOST")!


func requestReceiptForMigration(attachedTo originalUserId: String) -> AnyPublisher<Data, Error> {
SKStore()
.fetchReceiptURLPublisher()
.tryMap { receiptURL -> [String:String] in
let d = try Data(contentsOf: receiptURL, options: .alwaysMapped)
let receipt = d.base64EncodedString(options: [])
return ["receiptData": receipt,
"originalUserId": originalUserId]
}
.encode(encoder: JSONEncoder())
// TODO Map the error?
.map { data -> URLRequest in
var request = URLRequest(url: endpointURL)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = data
return request
}
.flatMap {
URLSession.shared.dataTaskPublisher(for: $0)
.tryMap { response -> Data in
guard let httpResponse = response as? HTTPURLResponse else {
throw ReceiptMigrationError.RequestError
}
let statusCode = httpResponse.statusCode
guard statusCode == 200 else {
switch statusCode {
case 409:
throw ReceiptMigrationError.ReceiptExists
case 400:
throw ReceiptMigrationError.APIError
default:
throw ReceiptMigrationError.RequestError
}
}
return response.data
}
}.eraseToAnyPublisher()
}

struct MigrationToken {
let token: String
let data: String


public func validateReceiptForMigration(attachedTo originalUserId: String) throws {
let dataComponents = data.components(separatedBy: ":")
let currentTimestamp = Int(Date().timeIntervalSince1970)

guard dataComponents.count == 3,
dataComponents[1] == originalUserId,
let receiptTimestamp = Int(dataComponents[2]),
// 5 min margin for timestamp
(currentTimestamp - receiptTimestamp) < 300,
isSignatureVerified else {
throw ReceiptMigrationError.InvalidMigrationReceipt
}
}

private let publicKeyStr = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsuI2ZyUFD45NRAH4OEu4GvrOmdv4X4Ti49pbhbLY2fvQNEHI6fp/5Ndawwnp5uK2GIDk0e1E//uV3GEiPT8vOA=="
private var publicKey: CryptoKit.P256.Signing.PublicKey {
get {
let pemKeyData = Data(base64Encoded: publicKeyStr)!

return (pemKeyData.withUnsafeBytes { bytes in
return try! CryptoKit.P256.Signing.PublicKey(derRepresentation: bytes)
})
}
}

private var isSignatureVerified: Bool {
guard
let data = data.data(using: .utf8),
let signedRawRS = Data(base64Encoded: token),
let signature = try? CryptoKit.P256.Signing
.ECDSASignature(rawRepresentation: signedRawRS) else {
return false
}

return publicKey.isValidSignature(signature, for: data as NSData)
}
}

enum ReceiptMigrationError: Error {
// 409 - we may want to drop the ID in this scenario.
case ReceiptExists
// 40X
case InvalidAppReceipt
case InvalidMigrationReceipt
// Everything else
case APIError
// Or capture NSURLError
case RequestError
}

enum SKStoreError: Error {
case notFound
case fetchError
case requestError(Error)
}

@objc class SKStore: NSObject {
var infoURL: URL { BlinkPaths.blinkURL().appendingPathComponent(".receiptInfo") }
var done: ((URL?, Error?) -> Void)!
var skReq: SKReceiptRefreshRequest? = nil

func fetchReceiptURLPublisher() -> AnyPublisher<URL, Error> {
return Future<URL, Error> { promise in
self.fetchReceiptURL { (url, error) in
if let url = url {
promise(.success(url))
} else {
promise(.failure(error ?? SKStoreError.fetchError))
}
}
}.eraseToAnyPublisher()
}

func fetchReceiptURL(_ done: @escaping (URL?, Error?) -> Void) {
self.done = done

guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else {
return done(nil, SKStoreError.notFound)
}
if !FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
let skReq = SKReceiptRefreshRequest(receiptProperties: nil)
skReq.delegate = self
skReq.start()
self.skReq = skReq
} else {
done(appStoreReceiptURL, nil)
}

}
}

extension SKStore: SKRequestDelegate {
func requestDidFinish(_ request: SKRequest) {
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
return done(appStoreReceiptURL, nil)
} else {
return done(nil, SKStoreError.notFound)
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
return done(nil, SKStoreError.requestError(error))
}
}

0 comments on commit 8f11c15

Please sign in to comment.