mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-07-01 17:53:36 +00:00
Compare commits
16 Commits
v2.4.0
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
1947b326a4 | |||
3df4bcef3c | |||
18ba03bf03 | |||
85a7a64bc9 | |||
409efa5f9f | |||
bb63ae8469 | |||
30c1d36974 | |||
de8d18f9e9 | |||
1ae0996e2c | |||
3d50a99430 | |||
45fc356f0f | |||
212678b94e | |||
2e1f4881a9 | |||
c2b80e3c7c | |||
15d2afd2cb | |||
2a4da36c4e |
BIN
.github/readme/localize_sidebar_agent.png
vendored
BIN
.github/readme/localize_sidebar_agent.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 260 KiB |
6
.github/workflows/nightly.yml
vendored
6
.github/workflows/nightly.yml
vendored
@ -5,8 +5,8 @@ on:
|
|||||||
- cron: "0 8 * * *"
|
- cron: "0 8 * * *"
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
# runs-on: macOS-latest
|
# runs-on: macOS-latest-xlarge
|
||||||
runs-on: macos-13
|
runs-on: macos-13-xlarge
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -20,7 +20,7 @@ jobs:
|
|||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
run: ./.github/scripts/signing.sh
|
run: ./.github/scripts/signing.sh
|
||||||
- name: Set Environment
|
- name: Set Environment
|
||||||
run: sudo xcrun xcode-select -s /Applications/Xcode_15.1.app
|
run: sudo xcrun xcode-select -s /Applications/Xcode_15.2.app
|
||||||
- name: Update Build Number
|
- name: Update Build Number
|
||||||
env:
|
env:
|
||||||
RUN_ID: ${{ github.run_id }}
|
RUN_ID: ${{ github.run_id }}
|
||||||
|
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@ -6,8 +6,8 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
# runs-on: macOS-latest
|
# runs-on: macOS-latest-xlarge
|
||||||
runs-on: macos-13
|
runs-on: macos-13-xlarge
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -21,7 +21,7 @@ jobs:
|
|||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
run: ./.github/scripts/signing.sh
|
run: ./.github/scripts/signing.sh
|
||||||
- name: Set Environment
|
- name: Set Environment
|
||||||
run: sudo xcrun xcode-select -s /Applications/Xcode_15.1.app
|
run: sudo xcrun xcode-select -s /Applications/Xcode_15.2.app
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
pushd Sources/Packages
|
pushd Sources/Packages
|
||||||
@ -43,7 +43,7 @@ jobs:
|
|||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
run: ./.github/scripts/signing.sh
|
run: ./.github/scripts/signing.sh
|
||||||
- name: Set Environment
|
- name: Set Environment
|
||||||
run: sudo xcrun xcode-select -s /Applications/Xcode_15.1.app
|
run: sudo xcrun xcode-select -s /Applications/Xcode_15.2.app
|
||||||
- name: Update Build Number
|
- name: Update Build Number
|
||||||
env:
|
env:
|
||||||
TAG_NAME: ${{ github.ref }}
|
TAG_NAME: ${{ github.ref }}
|
||||||
|
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@ -3,13 +3,13 @@ name: Test
|
|||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
# runs-on: macOS-latest
|
# runs-on: macOS-latest-xlarge
|
||||||
runs-on: macos-13
|
runs-on: macos-13-xlarge
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set Environment
|
- name: Set Environment
|
||||||
run: sudo xcrun xcode-select -s /Applications/Xcode_15.1.app
|
run: sudo xcrun xcode-select -s /Applications/Xcode_15.2.app
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
pushd Sources/Packages
|
pushd Sources/Packages
|
||||||
|
@ -16,7 +16,7 @@ Clone Secretive using [these instructions from GitHub](https://docs.github.com/e
|
|||||||
|
|
||||||
Open [Sources/Secretive.xcodeproj](Sources/Secretive.xcodeproj) in Xcode.
|
Open [Sources/Secretive.xcodeproj](Sources/Secretive.xcodeproj) in Xcode.
|
||||||
|
|
||||||
### Localize the Main App
|
### Translate
|
||||||
|
|
||||||
Navigate to [Secretive/Localizable](Sources/Secretive/Localizable.xcstrings).
|
Navigate to [Secretive/Localizable](Sources/Secretive/Localizable.xcstrings).
|
||||||
|
|
||||||
@ -28,14 +28,6 @@ If your language already has an in-progress localization, select it from the lis
|
|||||||
|
|
||||||
Start translating! You'll see a list of english phrases, and a space to add a translation of your language.
|
Start translating! You'll see a list of english phrases, and a space to add a translation of your language.
|
||||||
|
|
||||||
### Localize SecretAgent
|
|
||||||
|
|
||||||
Navigate to [Secretive/Localizable](Sources/SecretAgent/Localizable.xcstrings).
|
|
||||||
|
|
||||||
<img src="/.github/readme/localize_sidebar_agent.png" alt="Screenshot of Xcode navigating to the Localizable file" width="300">
|
|
||||||
|
|
||||||
Repeat the same steps from the process of localizing the main app.
|
|
||||||
|
|
||||||
### Create a Pull Request
|
### Create a Pull Request
|
||||||
|
|
||||||
Push your changes and open a pull request.
|
Push your changes and open a pull request.
|
||||||
|
@ -44,7 +44,7 @@ let package = Package(
|
|||||||
.target(
|
.target(
|
||||||
name: "SecureEnclaveSecretKit",
|
name: "SecureEnclaveSecretKit",
|
||||||
dependencies: ["SecretKit"],
|
dependencies: ["SecretKit"],
|
||||||
swiftSettings: [.enableExperimentalFeature("StrictConcurrency"), .unsafeFlags(["-warnings-as-errors"])]
|
swiftSettings: [.unsafeFlags(["-warnings-as-errors"])]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SmartCardSecretKit",
|
name: "SmartCardSecretKit",
|
||||||
|
@ -17,7 +17,7 @@ public protocol Secret: Identifiable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The type of algorithm the Secret uses. Currently, only elliptic curve algorithms are supported.
|
/// The type of algorithm the Secret uses. Currently, only elliptic curve algorithms are supported.
|
||||||
public enum Algorithm: Hashable {
|
public enum Algorithm: Hashable, Sendable {
|
||||||
|
|
||||||
case ellipticCurve
|
case ellipticCurve
|
||||||
case rsa
|
case rsa
|
||||||
|
@ -5,7 +5,7 @@ import SecretKit
|
|||||||
extension SecureEnclave {
|
extension SecureEnclave {
|
||||||
|
|
||||||
/// An implementation of Secret backed by the Secure Enclave.
|
/// An implementation of Secret backed by the Secure Enclave.
|
||||||
public struct Secret: SecretKit.Secret {
|
public struct Secret: SecretKit.Secret, Sendable {
|
||||||
|
|
||||||
public let id: Data
|
public let id: Data
|
||||||
public let name: String
|
public let name: String
|
||||||
|
@ -17,7 +17,7 @@ extension SecureEnclave {
|
|||||||
TKTokenWatcher().tokenIDs.contains("com.apple.setoken")
|
TKTokenWatcher().tokenIDs.contains("com.apple.setoken")
|
||||||
}
|
}
|
||||||
public let id = UUID()
|
public let id = UUID()
|
||||||
public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave")
|
public let name = String(localized: "secure_enclave")
|
||||||
@Published public private(set) var secrets: [Secret] = []
|
@Published public private(set) var secrets: [Secret] = []
|
||||||
|
|
||||||
private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:]
|
private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:]
|
||||||
@ -107,10 +107,10 @@ extension SecureEnclave {
|
|||||||
context = existing.context
|
context = existing.context
|
||||||
} else {
|
} else {
|
||||||
let newContext = LAContext()
|
let newContext = LAContext()
|
||||||
newContext.localizedCancelTitle = "Deny"
|
newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
|
||||||
context = newContext
|
context = newContext
|
||||||
}
|
}
|
||||||
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
|
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)")
|
||||||
let attributes = KeychainDictionary([
|
let attributes = KeychainDictionary([
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||||
@ -140,8 +140,8 @@ extension SecureEnclave {
|
|||||||
|
|
||||||
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
|
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
|
||||||
let context = LAContext()
|
let context = LAContext()
|
||||||
context.localizedReason = "verify a signature using secret \"\(secret.name)\""
|
context.localizedReason = String(localized: "auth_context_request_verify_description_\(secret.name)")
|
||||||
context.localizedCancelTitle = "Deny"
|
context.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
|
||||||
let attributes = KeychainDictionary([
|
let attributes = KeychainDictionary([
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||||
@ -180,22 +180,39 @@ extension SecureEnclave {
|
|||||||
|
|
||||||
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) throws {
|
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) throws {
|
||||||
let newContext = LAContext()
|
let newContext = LAContext()
|
||||||
newContext.touchIDAuthenticationAllowableReuseDuration = duration
|
newContext.touchIDAuthenticationAllowableReuseDuration = max(duration, LATouchIDAuthenticationMaximumAllowableReuseDuration)
|
||||||
newContext.localizedCancelTitle = "Deny"
|
newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
|
||||||
|
|
||||||
let formatter = DateComponentsFormatter()
|
let formatter = DateComponentsFormatter()
|
||||||
formatter.unitsStyle = .spellOut
|
formatter.unitsStyle = .spellOut
|
||||||
formatter.allowedUnits = [.hour, .minute, .day]
|
formatter.allowedUnits = [.hour, .minute, .day]
|
||||||
|
|
||||||
if let durationString = formatter.string(from: duration) {
|
if let durationString = formatter.string(from: duration) {
|
||||||
newContext.localizedReason = "unlock secret \"\(secret.name)\" for \(durationString)"
|
newContext.localizedReason = String(localized: "auth_context_persist_for_duration_\(secret.name)_\(durationString)")
|
||||||
} else {
|
} else {
|
||||||
newContext.localizedReason = "unlock secret \"\(secret.name)\""
|
newContext.localizedReason = String(localized: "auth_context_persist_for_duration_unknown_\(secret.name)")
|
||||||
}
|
}
|
||||||
newContext.evaluatePolicy(LAPolicy.deviceOwnerAuthentication, localizedReason: newContext.localizedReason) { [weak self] success, _ in
|
newContext.evaluatePolicy(LAPolicy.deviceOwnerAuthentication, localizedReason: newContext.localizedReason) { [weak self] success, _ in
|
||||||
guard success else { return }
|
guard success else { return }
|
||||||
let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration)
|
let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration)
|
||||||
self?.persistedAuthenticationContexts[secret] = context
|
self?.persistedAuthenticationContexts[secret] = context
|
||||||
|
// Contexts will expire within LATouchIDAuthenticationMaximumAllowableReuseDuration unless we periodically refresh them
|
||||||
|
if duration > LATouchIDAuthenticationMaximumAllowableReuseDuration {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
Timer.scheduledTimer(withTimeInterval: LATouchIDAuthenticationMaximumAllowableReuseDuration - 10, repeats: true) { [weak self] timer in
|
||||||
|
print("Refreshing context")
|
||||||
|
guard let refreshContext = self?.persistedAuthenticationContexts[secret] else { return }
|
||||||
|
guard refreshContext.valid else {
|
||||||
|
timer.invalidate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshContext.context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "Refresh") { success, _ in
|
||||||
|
guard success else { return }
|
||||||
|
print("Refreshed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,7 +277,7 @@ extension SecureEnclave.Store {
|
|||||||
nil)!
|
nil)!
|
||||||
|
|
||||||
let wrapped: [SecureEnclave.Secret] = publicTyped.map {
|
let wrapped: [SecureEnclave.Secret] = publicTyped.map {
|
||||||
let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
|
let name = $0[kSecAttrLabel] as? String ?? String(localized: "unnamed_secret")
|
||||||
let id = $0[kSecAttrApplicationLabel] as! Data
|
let id = $0[kSecAttrApplicationLabel] as! Data
|
||||||
let publicKeyRef = $0[kSecValueRef] as! SecKey
|
let publicKeyRef = $0[kSecValueRef] as! SecKey
|
||||||
let publicKeyAttributes = SecKeyCopyAttributes(publicKeyRef) as! [CFString: Any]
|
let publicKeyAttributes = SecKeyCopyAttributes(publicKeyRef) as! [CFString: Any]
|
||||||
|
@ -12,7 +12,7 @@ extension SmartCard {
|
|||||||
|
|
||||||
@Published public var isAvailable: Bool = false
|
@Published public var isAvailable: Bool = false
|
||||||
public let id = UUID()
|
public let id = UUID()
|
||||||
public private(set) var name = NSLocalizedString("Smart Card", comment: "Smart Card")
|
public private(set) var name = String(localized: "smart_card")
|
||||||
@Published public private(set) var secrets: [Secret] = []
|
@Published public private(set) var secrets: [Secret] = []
|
||||||
private let watcher = TKTokenWatcher()
|
private let watcher = TKTokenWatcher()
|
||||||
private var tokenID: String?
|
private var tokenID: String?
|
||||||
@ -50,8 +50,8 @@ extension SmartCard {
|
|||||||
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
|
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
|
||||||
guard let tokenID = tokenID else { fatalError() }
|
guard let tokenID = tokenID else { fatalError() }
|
||||||
let context = LAContext()
|
let context = LAContext()
|
||||||
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
|
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)")
|
||||||
context.localizedCancelTitle = "Deny"
|
context.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
|
||||||
let attributes = KeychainDictionary([
|
let attributes = KeychainDictionary([
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||||
@ -138,7 +138,7 @@ extension SmartCard.Store {
|
|||||||
private func loadSecrets() {
|
private func loadSecrets() {
|
||||||
guard let tokenID = tokenID else { return }
|
guard let tokenID = tokenID else { return }
|
||||||
|
|
||||||
let fallbackName = NSLocalizedString("Smart Card", comment: "Smart Card")
|
let fallbackName = String(localized: "smart_card")
|
||||||
if let driverName = watcher.tokenInfo(forTokenID: tokenID)?.driverName {
|
if let driverName = watcher.tokenInfo(forTokenID: tokenID)?.driverName {
|
||||||
name = driverName
|
name = driverName
|
||||||
} else {
|
} else {
|
||||||
@ -156,7 +156,7 @@ extension SmartCard.Store {
|
|||||||
SecItemCopyMatching(attributes, &untyped)
|
SecItemCopyMatching(attributes, &untyped)
|
||||||
guard let typed = untyped as? [[CFString: Any]] else { return }
|
guard let typed = untyped as? [[CFString: Any]] else { return }
|
||||||
let wrapped = typed.map {
|
let wrapped = typed.map {
|
||||||
let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
|
let name = $0[kSecAttrLabel] as? String ?? String(localized: "unnamed_secret")
|
||||||
let tokenID = $0[kSecAttrApplicationLabel] as! Data
|
let tokenID = $0[kSecAttrApplicationLabel] as! Data
|
||||||
let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber)
|
let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber)
|
||||||
let keySize = $0[kSecAttrKeySizeInBits] as! Int
|
let keySize = $0[kSecAttrKeySizeInBits] as! Int
|
||||||
@ -183,8 +183,8 @@ extension SmartCard.Store {
|
|||||||
/// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged.
|
/// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged.
|
||||||
public func encrypt(data: Data, with secret: SecretType) throws -> Data {
|
public func encrypt(data: Data, with secret: SecretType) throws -> Data {
|
||||||
let context = LAContext()
|
let context = LAContext()
|
||||||
context.localizedReason = "encrypt data using secret \"\(secret.name)\""
|
context.localizedReason = String(localized: "auth_context_request_encrypt_description_\(secret.name)")
|
||||||
context.localizedCancelTitle = "Deny"
|
context.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
|
||||||
let attributes = KeychainDictionary([
|
let attributes = KeychainDictionary([
|
||||||
kSecAttrKeyType: secret.algorithm.secAttrKeyType,
|
kSecAttrKeyType: secret.algorithm.secAttrKeyType,
|
||||||
kSecAttrKeySizeInBits: secret.keySize,
|
kSecAttrKeySizeInBits: secret.keySize,
|
||||||
@ -212,8 +212,8 @@ extension SmartCard.Store {
|
|||||||
public func decrypt(data: Data, with secret: SecretType) throws -> Data {
|
public func decrypt(data: Data, with secret: SecretType) throws -> Data {
|
||||||
guard let tokenID = tokenID else { fatalError() }
|
guard let tokenID = tokenID else { fatalError() }
|
||||||
let context = LAContext()
|
let context = LAContext()
|
||||||
context.localizedReason = "decrypt data using secret \"\(secret.name)\""
|
context.localizedReason = String(localized: "auth_context_request_decrypt_description_\(secret.name)")
|
||||||
context.localizedCancelTitle = "Deny"
|
context.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
|
||||||
let attributes = KeychainDictionary([
|
let attributes = KeychainDictionary([
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||||
|
@ -1,114 +0,0 @@
|
|||||||
{
|
|
||||||
"sourceLanguage" : "en",
|
|
||||||
"strings" : {
|
|
||||||
"persist_authentication_accept_button" : {
|
|
||||||
"comment" : "When the user authorizes an action using a secret that requires unlock, they're shown a notification offering to leave the secret unlocked for a set period of time. This is the title for the notification.",
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Leave Unlocked"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"persist_authentication_decline_button" : {
|
|
||||||
"comment" : "When the user authorizes an action using a secret that requires unlock, they're shown a notification offering to leave the secret unlocked for a set period of time. This is the decline button for the notification.",
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Do Not Unlock"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"signed_notification_description_%@" : {
|
|
||||||
"comment" : "When the user performs an action using a secret, they're shown a notification describing what happened. This is the description, showing which secret was used. The placeholder is the name of the secret.",
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Using secret %1$@"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"signed_notification_title_%@" : {
|
|
||||||
"comment" : "When the user performs an action using a secret, they're shown a notification describing what happened. This is the title, showing which app requested the action. The placeholder is the name of the app.",
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Signed Request from %1$@"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"update_notification_ignore_button" : {
|
|
||||||
"comment" : "When an update is available, a notification is shown. This is the button to decline an update.",
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Ignore"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"update_notification_update_button" : {
|
|
||||||
"comment" : "When an update is available, a notification is shown. This is the button to download an update.",
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Update"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"update_notification_update_critical_title_%@" : {
|
|
||||||
"comment" : "When an update is available, a notification is shown. This is the title for a very high priority update with security fixes. The placeholder is for the application version, eg \"Critical Security Update - 2.0\"",
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Critical Security Update - %1$@"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"update_notification_update_description" : {
|
|
||||||
"comment" : "When an update is available, a notification is shown. This is the description to download an update.",
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Click to Update"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"update_notification_update_normal_title_%@" : {
|
|
||||||
"comment" : "When an update is available, a notification is shown. This is the title for a normal priority update. The placeholder is for the application version, eg \"Update Available - 2.0\"",
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Update Available - %1$@"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"version" : "1.0"
|
|
||||||
}
|
|
@ -19,7 +19,6 @@
|
|||||||
5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */; };
|
5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */; };
|
||||||
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF642780081B00DF2006 /* SmartCardSecretKit */; };
|
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF642780081B00DF2006 /* SmartCardSecretKit */; };
|
||||||
500B93C32B478D8400E157DE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 500B93C22B478D8400E157DE /* Localizable.xcstrings */; };
|
500B93C32B478D8400E157DE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 500B93C22B478D8400E157DE /* Localizable.xcstrings */; };
|
||||||
500B93C72B479E2E00E157DE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 500B93C62B479E2E00E157DE /* Localizable.xcstrings */; };
|
|
||||||
501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; };
|
501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; };
|
||||||
501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
||||||
@ -53,6 +52,7 @@
|
|||||||
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; };
|
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; };
|
||||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; };
|
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; };
|
||||||
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
|
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
|
||||||
|
50E9CF422B51D596004AB36D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 500B93C22B478D8400E157DE /* Localizable.xcstrings */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@ -111,7 +111,6 @@
|
|||||||
50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
|
50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
|
||||||
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
|
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
|
||||||
500B93C22B478D8400E157DE /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
500B93C22B478D8400E157DE /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||||
500B93C62B479E2E00E157DE /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
|
||||||
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
||||||
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
||||||
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
||||||
@ -316,7 +315,6 @@
|
|||||||
50A3B79824026B7600D209EA /* Info.plist */,
|
50A3B79824026B7600D209EA /* Info.plist */,
|
||||||
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */,
|
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */,
|
||||||
50A3B79924026B7600D209EA /* SecretAgent.entitlements */,
|
50A3B79924026B7600D209EA /* SecretAgent.entitlements */,
|
||||||
500B93C62B479E2E00E157DE /* Localizable.xcstrings */,
|
|
||||||
50A3B79224026B7600D209EA /* Preview Content */,
|
50A3B79224026B7600D209EA /* Preview Content */,
|
||||||
);
|
);
|
||||||
path = SecretAgent;
|
path = SecretAgent;
|
||||||
@ -431,6 +429,10 @@
|
|||||||
knownRegions = (
|
knownRegions = (
|
||||||
en,
|
en,
|
||||||
Base,
|
Base,
|
||||||
|
it,
|
||||||
|
fr,
|
||||||
|
de,
|
||||||
|
"pt-BR",
|
||||||
);
|
);
|
||||||
mainGroup = 50617D7623FCE48D0099B055;
|
mainGroup = 50617D7623FCE48D0099B055;
|
||||||
productRefGroup = 50617D8023FCE48E0099B055 /* Products */;
|
productRefGroup = 50617D8023FCE48E0099B055 /* Products */;
|
||||||
@ -469,7 +471,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
50A3B79724026B7600D209EA /* Main.storyboard in Resources */,
|
50A3B79724026B7600D209EA /* Main.storyboard in Resources */,
|
||||||
500B93C72B479E2E00E157DE /* Localizable.xcstrings in Resources */,
|
50E9CF422B51D596004AB36D /* Localizable.xcstrings in Resources */,
|
||||||
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */,
|
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */,
|
||||||
50A3B79124026B7600D209EA /* Assets.xcassets in Resources */,
|
50A3B79124026B7600D209EA /* Assets.xcassets in Resources */,
|
||||||
508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */,
|
508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -26,7 +26,7 @@ struct SetupView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(idealWidth: 500, idealHeight: 500)
|
.frame(minWidth: 500, idealWidth: 500, minHeight: 500, idealHeight: 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,16 +42,16 @@ struct UpdateDetailView<UpdaterType: Updater>: View {
|
|||||||
if let prefix = split.first {
|
if let prefix = split.first {
|
||||||
switch prefix {
|
switch prefix {
|
||||||
case "#":
|
case "#":
|
||||||
attributed = Text(unprefixed).font(.title) + Text("\n")
|
attributed = Text(unprefixed).font(.title) + Text(verbatim: "\n")
|
||||||
case "##":
|
case "##":
|
||||||
attributed = Text(unprefixed).font(.title2) + Text("\n")
|
attributed = Text(unprefixed).font(.title2) + Text(verbatim: "\n")
|
||||||
case "###":
|
case "###":
|
||||||
attributed = Text(unprefixed).font(.title3) + Text("\n")
|
attributed = Text(unprefixed).font(.title3) + Text(verbatim: "\n")
|
||||||
default:
|
default:
|
||||||
attributed = Text(line) + Text("\n\n")
|
attributed = Text(line) + Text(verbatim: "\n\n")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
attributed = Text(line) + Text("\n\n")
|
attributed = Text(line) + Text(verbatim: "\n\n")
|
||||||
}
|
}
|
||||||
text = text + attributed
|
text = text + attributed
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user