Skip to content

Commit

Permalink
Merge pull request #24 from Tinder/maxwelle/enterprise-support
Browse files Browse the repository at this point in the history
Adds enterprise support
  • Loading branch information
tinder-maxwellelliott authored Jan 15, 2025
2 parents f8f1317 + e7248df commit d3308fa
Show file tree
Hide file tree
Showing 44 changed files with 827 additions and 300 deletions.
4 changes: 3 additions & 1 deletion .bazelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
common --enable_bzlmod

build --incompatible_disallow_empty_glob
build --apple_platform_type=macos
build --incompatible_strict_action_env
Expand All @@ -8,4 +10,4 @@ build --crosstool_top=@local_config_apple_cc//:toolchain
build --host_crosstool_top=@local_config_apple_cc//:toolchain
test --test_output=errors
test --test_summary=detailed
common --enable_bzlmod
test:record_snapshots --spawn_strategy=local
1 change: 1 addition & 0 deletions .bazelversion
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
7.3.1
1 change: 0 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@ bazel_dep(name = "swift_argument_parser", version = "1.3.0", repo_name = "com_gi
non_module_dependencies = use_extension("//:extensions.bzl", "non_module_dependencies")
use_repo(
non_module_dependencies,
"com_github_kitura_blueecc",
"com_github_kylef_pathkit",
)
20 changes: 5 additions & 15 deletions MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.PHONY: record_snapshots
record_snapshots:
bazel test //Tests/... \
--config=record_snapshots \
--test_env=BUILD_WORKSPACE_DIRECTORY=$$(pwd) \
--test_env=SNAPSHOT_DIRECTORY="$$(pwd)/Tests/SignHereLibraryTests" \
--test_env=RERECORD_SNAPSHOTS=TRUE
1 change: 0 additions & 1 deletion Sources/SignHereLibrary/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ swift_library(
deps = [
"//Sources/CoreLibrary",
"@com_github_apple_swift_argument_parser//:ArgumentParser",
"@com_github_kitura_blueecc//:BlueECC",
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
case certificateSigningRequestSubject = "certificateSigningRequestSubject"
case profileName = "profileName"
case autoRegenerate = "autoRegenerate"
case enterprise = "enterprise"
}

@Option(help: "The key identifier of the private key (https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests)")
Expand Down Expand Up @@ -189,6 +190,9 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
@Flag(help: "Defines if the profile should be regenerated in case it already exists (optional)")
internal var autoRegenerate = false

@Flag(help: "Controls if the enterprise API should be used.")
internal var enterprise: Bool = false

private let files: Files
private let log: Log
private let shell: Shell
Expand All @@ -206,10 +210,7 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
shell = shellImp
uuid = UUIDImp()
iTunesConnectService = iTunesConnectServiceImp(
network: NetworkImp(),
files: filesImp,
shell: shellImp,
clock: clockImp
enterprise: false
)
}

Expand All @@ -236,7 +237,8 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
bundleIdentifierName: String?,
platform: String,
profileName: String?,
autoRegenerate: Bool
autoRegenerate: Bool,
enterprise: Bool
) {
self.files = files
self.log = log
Expand All @@ -261,24 +263,23 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
self.platform = platform
self.profileName = profileName
self.autoRegenerate = autoRegenerate
self.enterprise = enterprise
}

internal init(from decoder: Decoder) throws {
let filesImp: Files = FilesImp()
let clockImp: Clock = ClockImp()
let shellImp: Shell = ShellImp()
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
let enterprise: Bool = try container.decode(Bool.self, forKey: .enterprise)
self.init(
files: filesImp,
log: LogImp(),
jsonWebTokenService: JSONWebTokenServiceImp(clock: clockImp),
shell: shellImp,
uuid: UUIDImp(),
iTunesConnectService: iTunesConnectServiceImp(
network: NetworkImp(),
files: filesImp,
shell: shellImp,
clock: clockImp
enterprise: enterprise
),
keyIdentifier: try container.decode(String.self, forKey: .keyIdentifier),
issuerID: try container.decode(String.self, forKey: .issuerID),
Expand All @@ -296,15 +297,17 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
bundleIdentifierName: try container.decodeIfPresent(String.self, forKey: .bundleIdentifierName),
platform: try container.decode(String.self, forKey: .platform),
profileName: try container.decodeIfPresent(String.self, forKey: .profileName),
autoRegenerate: try container.decode(Bool.self, forKey: .autoRegenerate)
autoRegenerate: try container.decode(Bool.self, forKey: .autoRegenerate),
enterprise: enterprise
)
}

internal func run() throws {
let jsonWebToken: String = try jsonWebTokenService.createToken(
keyIdentifier: keyIdentifier,
issuerID: issuerID,
secretKey: try files.read(Path(itunesConnectKeyPath))
secretKey: try files.read(Path(itunesConnectKeyPath)),
enterprise: enterprise
)
let deviceIDs: Set<String> = try iTunesConnectService.fetchITCDeviceIDs(jsonWebToken: jsonWebToken)
guard let profileName, let profile = try? fetchProvisioningProfile(jsonWebToken: jsonWebToken, name: profileName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal struct DeleteProvisioningProfileCommand: ParsableCommand {
case keyIdentifier = "keyIdentifier"
case issuerID = "issuerID"
case itunesConnectKeyPath = "itunesConnectKeyPath"
case enterprise = "enterprise"
}

@Option(help: "The iTunes Connect API ID of the provisioning profile to delete (https://developer.apple.com/documentation/appstoreconnectapi/profile)")
Expand All @@ -38,6 +39,9 @@ internal struct DeleteProvisioningProfileCommand: ParsableCommand {
@Option(help: "The path to the private key (https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests)")
internal var itunesConnectKeyPath: String

@Flag(help: "Controls if the enterprise API should be used.")
internal var enterprise: Bool = false

private let files: Files
private let jsonWebTokenService: JSONWebTokenService
private let iTunesConnectService: iTunesConnectService
Expand All @@ -47,10 +51,7 @@ internal struct DeleteProvisioningProfileCommand: ParsableCommand {
files = filesImp
jsonWebTokenService = JSONWebTokenServiceImp(clock: ClockImp())
iTunesConnectService = iTunesConnectServiceImp(
network: NetworkImp(),
files: filesImp,
shell: ShellImp(),
clock: ClockImp()
enterprise: false
)
}

Expand All @@ -61,7 +62,8 @@ internal struct DeleteProvisioningProfileCommand: ParsableCommand {
provisioningProfileId: String,
keyIdentifier: String,
issuerID: String,
itunesConnectKeyPath: String
itunesConnectKeyPath: String,
enterprise: Bool
) {
self.files = files
self.jsonWebTokenService = jsonWebTokenService
Expand All @@ -70,32 +72,33 @@ internal struct DeleteProvisioningProfileCommand: ParsableCommand {
self.keyIdentifier = keyIdentifier
self.issuerID = issuerID
self.itunesConnectKeyPath = itunesConnectKeyPath
self.enterprise = enterprise
}

internal init(from decoder: Decoder) throws {
let filesImp: Files = FilesImp()
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
let enterprise: Bool = try container.decode(Bool.self, forKey: .enterprise)
self.init(
files: filesImp,
jsonWebTokenService: JSONWebTokenServiceImp(clock: ClockImp()),
iTunesConnectService: iTunesConnectServiceImp(
network: NetworkImp(),
files: filesImp,
shell: ShellImp(),
clock: ClockImp()
enterprise: enterprise
),
provisioningProfileId: try container.decode(String.self, forKey: .provisioningProfileId),
keyIdentifier: try container.decode(String.self, forKey: .keyIdentifier),
issuerID: try container.decode(String.self, forKey: .issuerID),
itunesConnectKeyPath: try container.decode(String.self, forKey: .itunesConnectKeyPath)
itunesConnectKeyPath: try container.decode(String.self, forKey: .itunesConnectKeyPath),
enterprise: enterprise
)
}

internal func run() throws {
let jsonWebToken: String = try jsonWebTokenService.createToken(
keyIdentifier: keyIdentifier,
issuerID: issuerID,
secretKey: try files.read(Path(itunesConnectKeyPath))
secretKey: try files.read(Path(itunesConnectKeyPath)),
enterprise: enterprise
)
try iTunesConnectService.deleteProvisioningProfile(
jsonWebToken: jsonWebToken,
Expand Down
2 changes: 1 addition & 1 deletion Sources/SignHereLibrary/Models/ProfileType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ enum ProfileType {

var usesDevices: Bool {
switch self {
case .appStore: return false
case .appStore, .inHouse: return false
default: return true
}
}
Expand Down
24 changes: 11 additions & 13 deletions Sources/SignHereLibrary/Services/JSONWebTokenService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import CoreLibrary
import Foundation
import CryptorECC
import CryptoKit

// ME: Documented here https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests

Expand All @@ -16,7 +16,8 @@ internal protocol JSONWebTokenService {
func createToken(
keyIdentifier: String,
issuerID: String,
secretKey: Data
secretKey: Data,
enterprise: Bool
) throws -> String
}

Expand Down Expand Up @@ -44,7 +45,8 @@ internal class JSONWebTokenServiceImp: JSONWebTokenService {
func createToken(
keyIdentifier: String,
issuerID: String,
secretKey: Data
secretKey: Data,
enterprise: Bool
) throws -> String {
let header: JSONWebTokenHeader = .init(
alg: "ES256",
Expand All @@ -57,33 +59,29 @@ internal class JSONWebTokenServiceImp: JSONWebTokenService {
iss: issuerID,
iat: currentTime.timeIntervalSince1970,
exp: expirationTime.timeIntervalSince1970,
aud: "appstoreconnect-v1"
aud: enterprise ? "apple-developer-enterprise-v1" : "appstoreconnect-v1"
)
let jsonEncoder: JSONEncoder = .init()
var components: [String] = [
urlBase64Encode(data: try jsonEncoder.encode(header)),
".",
urlBase64Encode(data: try jsonEncoder.encode(payload)),
]
let signature: String = try createSignedHeaderPayload(data: Data(components.joined().utf8), secretKey: secretKey)
let signature: String = try createSignature(data: Data(components.joined().utf8), secretKey: secretKey)
components.append(contentsOf: [
".",
signature
])
return components.joined()
}

private func createSignedHeaderPayload(data: Data, secretKey: Data) throws -> String {
private func createSignature(data: Data, secretKey: Data) throws -> String {
guard let keyString = String(data: secretKey, encoding: .utf8) else {
throw Error.unableToCreateKeyString
}
let privateKey: ECPrivateKey = try .init(key: keyString)
guard privateKey.curve == .prime256v1
else {
throw Error.unableToCreatePrivateKey
}
let signedData: ECSignature = try data.sign(with: privateKey)
return urlBase64Encode(data: signedData.r + signedData.s)
let key = try P256.Signing.PrivateKey(pemRepresentation: keyString)
let signature = try key.signature(for: data)
return urlBase64Encode(data: signature.rawRepresentation)
}

private func urlBase64Encode(data: Data) -> String {
Expand Down
Loading

0 comments on commit d3308fa

Please sign in to comment.