Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EUID Support #57

Merged
merged 3 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
<dict>
<key>UID2EnvironmentEUID</key>
<false/>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

"common.nil" = "Nil";

"root.navigation.title" = "UID2 SDK Dev App";
"root.uid2.navigation.title" = "UID2 SDK Dev App";
"root.euid.navigation.title" = "EUID SDK Dev App";
"root.title.identitypackage" = "Current Identity";
"root.label.error" = "Error Occurred";
"root.button.reset" = "Reset";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ internal final class AppUID2Client: Sendable {
func decryptResponse(_ b64Secret: String, _ responseData: Data, _ isRefresh: Bool = false) -> Data? {

// Confirm that responseData is Base64
// swiftlint:disable:next non_optional_string_data_conversion
guard let base64String = String(data: responseData, encoding: .utf8),
let decodedData = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters) else {
return responseData
Expand Down Expand Up @@ -211,6 +212,7 @@ internal final class AppUID2Client: Sendable {
payload = decryptedData.subdata(in: 16..<decryptedData.count)
}

// swiftlint:disable:next non_optional_string_data_conversion
guard let _ = String(data: payload, encoding: .utf8) else {
return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ struct RootView: View {
var body: some View {

VStack {
Text("root.navigation.title")
Text(viewModel.isEUID ? "root.euid.navigation.title" : "root.uid2.navigation.title")
.font(Font.system(size: 28, weight: .bold))
HStack {
TextField("Email Address", text: $email)
Expand Down Expand Up @@ -85,6 +85,7 @@ extension TokenGenerationError: LocalizedError {
if let message,
let jsonObject = try? JSONSerialization.jsonObject(with: Data(message.utf8)),
let jsonString = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted) {
// swiftlint:disable:next non_optional_string_data_conversion
formattedMessage = String(data: jsonString, encoding: .utf8)
} else {
formattedMessage = message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,41 @@

import Combine
import Foundation
import OSLog
import SwiftUI
import UID2

extension RootViewModel {
struct Configuration {
let subscriptionID: String
let appName: String
let serverPublicKeyString: String

static func uid2() -> Self {
self.init(
subscriptionID: "toPh8vgJgt",
appName: Bundle.main.bundleIdentifier!,
// swiftlint:disable:next line_length
serverPublicKeyString: "UID2-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKAbPfOz7u25g1fL6riU7p2eeqhjmpALPeYoyjvZmZ1xM2NM8UeOmDZmCIBnKyRZ97pz5bMCjrs38WM22O7LJuw=="
)
}

static func euid() -> Self {
self.init(
subscriptionID: "w6yPQzN4dA",
appName: Bundle.main.bundleIdentifier!,
// swiftlint:disable:next line_length
serverPublicKeyString: "EUID-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEH/k7HYGuWhjhCo8nXgj/ypClo5kek7uRKvzCGwj04Y1eXOWmHDOLAQVCPquZdfVVezIpABNAl9zvsSEC7g+ZGg=="
)
}
}
}

@MainActor
class RootViewModel: ObservableObject {

final class RootViewModel: ObservableObject {

let isEUID: Bool

@Published private(set) var uid2Identity: UID2Identity? {
didSet {
error = nil
Expand All @@ -25,19 +54,33 @@ class RootViewModel: ObservableObject {

/// `UID2Settings` must be configured prior to accessing the `UID2Manager` instance.
/// Configuring them here makes it less likely that an access occurs before configuration.
private let manager: UID2Manager = {
private let manager: UID2Manager

private let configuration: Configuration

private let log = OSLog(subsystem: "com.uid2.UID2SDKDevelopmentApp", category: "RootViewModel")

init() {
isEUID = Bundle.main.object(forInfoDictionaryKey: "UID2EnvironmentEUID") as? Bool ?? false

UID2Settings.shared.isLoggingEnabled = true
// Only the development app should use the integration environment.
// If you have copied the dev app for testing, you probably want to use the default
// environment, which is production.
if Bundle.main.bundleIdentifier == "com.uid2.UID2SDKDevelopmentApp" {
UID2Settings.shared.environment = .custom(url: URL(string: "https://operator-integ.uidapi.com")!)
UID2Settings.shared.euidEnvironment = .custom(url: URL(string: "https://integ.euid.eu/v2")!)
UID2Settings.shared.uid2Environment = .custom(url: URL(string: "https://operator-integ.uidapi.com")!)
}

return UID2Manager.shared
}()

init() {
if isEUID {
os_log("Configured for EUID", log: log, type: .info)
configuration = .euid()
manager = EUIDManager.shared
} else {
os_log("Configured for UID2", log: log, type: .info)
configuration = .uid2()
manager = UID2Manager.shared
}
Task {
for await state in await manager.stateValues() {
self.uid2Identity = state?.identity
Expand Down Expand Up @@ -130,17 +173,13 @@ class RootViewModel: ObservableObject {
}

func clientSideGenerate(identity: IdentityType) {
let subscriptionID = "toPh8vgJgt"
// swiftlint:disable:next line_length
let serverPublicKeyString = "UID2-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKAbPfOz7u25g1fL6riU7p2eeqhjmpALPeYoyjvZmZ1xM2NM8UeOmDZmCIBnKyRZ97pz5bMCjrs38WM22O7LJuw=="

Task<Void, Never> {
do {
try await manager.generateIdentity(
identity,
subscriptionID: subscriptionID,
serverPublicKey: serverPublicKeyString,
appName: Bundle.main.bundleIdentifier!
subscriptionID: configuration.subscriptionID,
serverPublicKey: configuration.serverPublicKeyString,
appName: configuration.appName
)
} catch {
self.error = error
Expand Down
17 changes: 17 additions & 0 deletions Sources/UID2/EUIDManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// EUIDManager.swift
//

import Foundation

public final class EUIDManager {

/// Singleton access point for EUID Manager
/// Returns a manager configured for use with EUID.
public static let shared: UID2Manager = {
UID2Manager(
environment: Environment(UID2Settings.shared.euidEnvironment),
account: .euid
)
}()
}
88 changes: 67 additions & 21 deletions Sources/UID2/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,74 @@

import Foundation

/// For more information, see https://unifiedid.com/docs/getting-started/gs-environments
public struct Environment: Hashable, Sendable {
/// Internal Environment representation
struct Environment: Hashable, Sendable {

/// API base URL
var endpoint: URL

/// Equivalent to `ohio`
public static let production = ohio

/// AWS US East (Ohio)
public static let ohio = Self(endpoint: URL(string: "https://prod.uidapi.com")!)
/// AWS US West (Oregon)
public static let oregon = Self(endpoint: URL(string: "https://usw.prod.uidapi.com")!)
/// AWS Asia Pacific (Singapore)
public static let singapore = Self(endpoint: URL(string: "https://sg.prod.uidapi.com")!)
/// AWS Asia Pacific (Sydney)
public static let sydney = Self(endpoint: URL(string: "https://au.prod.uidapi.com")!)
/// AWS Asia Pacific (Tokyo)
public static let tokyo = Self(endpoint: URL(string: "https://jp.prod.uidapi.com")!)

/// A custom endpoint
public static func custom(url: URL) -> Self {
Self(endpoint: url)
let endpoint: URL
let isProduction: Bool
}

extension Environment {
init(_ environment: UID2.Environment) {
endpoint = environment.endpoint
isProduction = (environment == .production)
}

init(_ environment: EUID.Environment) {
endpoint = environment.endpoint
isProduction = (environment == .production)
}
}

// Namespaces
public enum EUID {}
public enum UID2 {}

extension UID2 {
/// For more information, see https://unifiedid.com/docs/getting-started/gs-environments
public struct Environment: Hashable, Sendable {

/// API base URL
var endpoint: URL

/// Equivalent to `ohio`
public static let production = ohio

/// AWS US East (Ohio)
public static let ohio = Self(endpoint: URL(string: "https://prod.uidapi.com")!)
/// AWS US West (Oregon)
public static let oregon = Self(endpoint: URL(string: "https://usw.prod.uidapi.com")!)
/// AWS Asia Pacific (Singapore)
public static let singapore = Self(endpoint: URL(string: "https://sg.prod.uidapi.com")!)
/// AWS Asia Pacific (Sydney)
public static let sydney = Self(endpoint: URL(string: "https://au.prod.uidapi.com")!)
/// AWS Asia Pacific (Tokyo)
public static let tokyo = Self(endpoint: URL(string: "https://jp.prod.uidapi.com")!)

/// A custom endpoint
public static func custom(url: URL) -> Self {
Self(endpoint: url)
}
}
}

extension EUID {
/// See https://euid.eu/docs/getting-started/gs-environments
public struct Environment: Hashable, Sendable {

/// API base URL
var endpoint: URL

/// Equivalent to `london`
public static let production = london

/// AWS EU West 2 (London)
public static let london = Self(endpoint: URL(string: "https://prod.euid.eu/v2")!)

/// A custom endpoint
public static func custom(url: URL) -> Self {
Self(endpoint: url)
}
}
}
18 changes: 14 additions & 4 deletions Sources/UID2/KeychainManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import Foundation
import Security

extension Storage {
static func keychainStorage() -> Storage {
let storage = KeychainManager()
static func keychainStorage(account: Account) -> Storage {
let storage = KeychainManager(account: account)
return .init(
loadIdentity: { await storage.loadIdentity() },
saveIdentity: { await storage.saveIdentity($0) },
Expand All @@ -16,13 +16,23 @@ extension Storage {
}
}

/// These RawValue are used as persistence keys and must not be renamed
enum Account: String {
case uid2 = "uid2" // swiftlint:disable:this redundant_string_enum_value
case euid = "euid" // swiftlint:disable:this redundant_string_enum_value
}

/// Securely manages data in the Keychain
actor KeychainManager {

private let attrAccount = "uid2"
private let attrAccount: Account

private static let attrService = "auth-state"

init(account: Account = .uid2) {
attrAccount = account
}

func loadIdentity() -> IdentityPackage? {
let query = query(with: [
String(kSecReturnData): true
Expand Down Expand Up @@ -77,7 +87,7 @@ actor KeychainManager {
private func query(with queryElements: [String: Any]) -> CFDictionary {
let commonElements = [
String(kSecClass): kSecClassGenericPassword,
String(kSecAttrAccount): attrAccount,
String(kSecAttrAccount): attrAccount.rawValue,
String(kSecAttrService): Self.attrService
] as [String: Any]

Expand Down
2 changes: 2 additions & 0 deletions Sources/UID2/Networking/DataEnvelope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ internal enum DataEnvelope {
extension Data {
/// A convenience initializer for converting from a Data representation of a base64 encoded string to its decoded Data.
init?(base64EncodedData: Data, options: Data.Base64DecodingOptions = []) {
// https://github.com/realm/SwiftLint/issues/5263#issuecomment-2115182747
// swiftlint:disable:next non_optional_string_data_conversion
caroline-ttd marked this conversation as resolved.
Show resolved Hide resolved
guard let base64String = String(data: base64EncodedData, encoding: .utf8) else {
return nil
}
Expand Down
6 changes: 4 additions & 2 deletions Sources/UID2/UID2Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal final class UID2Client: Sendable {
init(
sdkVersion: String,
isLoggingEnabled: Bool = false,
environment: Environment = .production,
environment: Environment,
session: NetworkSession = URLSession.shared,
cryptoUtil: CryptoUtil = .liveValue
) {
Expand Down Expand Up @@ -120,9 +120,11 @@ internal final class UID2Client: Sendable {
let decoder = JSONDecoder.apiDecoder()
guard response.statusCode == 200 else {
let statusCode = response.statusCode
// https://github.com/realm/SwiftLint/issues/5263#issuecomment-2115182747
// swiftlint:disable:next non_optional_string_data_conversion
let responseText = String(data: data, encoding: .utf8) ?? "<none>"
os_log("Request failure (%d) %@", log: log, type: .error, statusCode, responseText)
if environment != .production {
if !environment.isProduction {
os_log("Failed request is using non-production API endpoint %@, is this intentional?", log: log, type: .error, baseURL.description)
}
throw TokenGenerationError.requestFailure(
Expand Down
Loading