diff --git a/Example/JustTweak/Accessors/GeneratedTweakAccessor+Constants.swift b/Example/JustTweak/Accessors/GeneratedTweakAccessor+Constants.swift index 28cd3a4..612caab 100644 --- a/Example/JustTweak/Accessors/GeneratedTweakAccessor+Constants.swift +++ b/Example/JustTweak/Accessors/GeneratedTweakAccessor+Constants.swift @@ -17,6 +17,7 @@ extension GeneratedTweakAccessor { static let displayGreenView = "display_green_view" static let displayRedView = "display_red_view" static let displayYellowView = "display_yellow_view" + static let encryptedAnswerToTheUniverse = "encrypted_answer_to_the_universe" static let greetOnAppDidBecomeActive = "greet_on_app_did_become_active" static let labelText = "label_text" static let redViewAlphaComponent = "red_view_alpha_component" diff --git a/Example/JustTweak/Accessors/GeneratedTweakAccessor.swift b/Example/JustTweak/Accessors/GeneratedTweakAccessor.swift index f09d6ef..413ddfa 100644 --- a/Example/JustTweak/Accessors/GeneratedTweakAccessor.swift +++ b/Example/JustTweak/Accessors/GeneratedTweakAccessor.swift @@ -29,6 +29,11 @@ class GeneratedTweakAccessor { set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.displayYellowView) } } + var definitiveAnswerEncrypted: String { + get { tweakManager.tweakWith(feature: Features.general, variable: Variables.encryptedAnswerToTheUniverse)?.stringValue ?? "" } + set { tweakManager.set(newValue, feature: Features.general, variable: Variables.encryptedAnswerToTheUniverse) } + } + var isTapGestureToChangeColorEnabled: Bool { get { tweakManager.tweakWith(feature: Features.general, variable: Variables.tapToChangeColorEnabled)?.boolValue ?? false } set { tweakManager.set(newValue, feature: Features.general, variable: Variables.tapToChangeColorEnabled) } diff --git a/Example/JustTweak/Code/AppDelegate.swift b/Example/JustTweak/Code/AppDelegate.swift index d678a25..0c0c187 100644 --- a/Example/JustTweak/Code/AppDelegate.swift +++ b/Example/JustTweak/Code/AppDelegate.swift @@ -60,6 +60,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let tweakManager = TweakManager(tweakProviders: tweakProviders) tweakManager.useCache = true + + tweakManager.decryptionClosure = { tweak in + String((tweak.value.stringValue ?? "").reversed()) + } + return tweakManager } } diff --git a/Example/JustTweak/Code/ViewController.swift b/Example/JustTweak/Code/ViewController.swift index 30399e9..896ade7 100644 --- a/Example/JustTweak/Code/ViewController.swift +++ b/Example/JustTweak/Code/ViewController.swift @@ -32,6 +32,12 @@ class ViewController: UIViewController { print("Tweak changed: \(tweak)") self?.updateView() } + addEncryptedMeaningOfLifeTapGesture() + } + + private func addEncryptedMeaningOfLifeTapGesture() { + let tapGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(showEncryptedMeaningOfLife)) + view.addGestureRecognizer(tapGestureRecognizer) } internal func updateView() { @@ -60,6 +66,14 @@ class ViewController: UIViewController { present(alertController, animated: true, completion: nil) } + @objc func showEncryptedMeaningOfLife() { + let alertController = UIAlertController(title: "Encrypted Meaning of Life", + message: String(describing: tweakAccessor.definitiveAnswerEncrypted), + preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "OK", style: .default)) + present(alertController, animated: true) + } + @objc internal func changeViewColor() { func randomColorValue() -> CGFloat { return CGFloat(arc4random() % 255) / 255.0 diff --git a/Example/JustTweak/TweakProviders/LocalTweaks_example.json b/Example/JustTweak/TweakProviders/LocalTweaks_example.json index 25b12f7..b7283c6 100644 --- a/Example/JustTweak/TweakProviders/LocalTweaks_example.json +++ b/Example/JustTweak/TweakProviders/LocalTweaks_example.json @@ -36,6 +36,14 @@ } }, "general": { + "encrypted_answer_to_the_universe": { + "Title": "Encrypted definitive answer", + "Description": "Encrypted answer to the Ultimate Question of Life, the Universe, and Everything", + "Group": "General", + "Value": "24 ton yletinifeD", + "GeneratedPropertyName": "definitiveAnswerEncrypted", + "Encrypted": true + }, "answer_to_the_universe": { "Title": "Definitive answer", "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything", diff --git a/Example/Tests/Core/TweakManagerTests.swift b/Example/Tests/Core/TweakManagerTests.swift index bd4381f..3b8a1cc 100644 --- a/Example/Tests/Core/TweakManagerTests.swift +++ b/Example/Tests/Core/TweakManagerTests.swift @@ -90,25 +90,19 @@ class TweakManagerTests: XCTestCase { } func testTweakManagerDecryption() throws { - var mutableTweakProvider = try XCTUnwrap(tweakManager.mutableTweakProvider) - let feature = "password" - let variable = "variable" + let url = try XCTUnwrap(Bundle.main.url(forResource: "LocalTweaks_example", withExtension: "json")) - let encodedString = try XCTUnwrap("my secret password".data(using: .utf8)?.base64EncodedString()) + tweakManager.tweakProviders.append(LocalTweakProvider(jsonURL: url)) - mutableTweakProvider.set(encodedString, feature: feature, variable: variable) - mutableTweakProvider.decryptionClosure = { tweak in - let data = Data(base64Encoded: tweak.stringValue ?? "").map { - String(data: $0, encoding: .utf8) - } ?? "" - - return data ?? "" + tweakManager.decryptionClosure = { tweak in + String((tweak.value.stringValue ?? "").reversed()) } + let feature = "general" + let variable = "encrypted_answer_to_the_universe" let tweak = tweakManager.tweakWith(feature: feature, variable: variable) - XCTAssertEqual("bXkgc2VjcmV0IHBhc3N3b3Jk", tweak?.stringValue) - XCTAssertEqual("my secret password", tweak?.decryptedValue?.stringValue) + XCTAssertEqual("Definitely not 42", tweak?.stringValue) } func testSetTweakManagerDecryptionClosureThenDecryptionClosureIsSetForProviders() throws { diff --git a/JustTweak/Classes/DTOs/Tweak.swift b/JustTweak/Classes/DTOs/Tweak.swift index 6c5a006..d770581 100644 --- a/JustTweak/Classes/DTOs/Tweak.swift +++ b/JustTweak/Classes/DTOs/Tweak.swift @@ -11,7 +11,6 @@ public struct Tweak { public let variable: String public let value: TweakValue - public let decryptedValue: TweakValue? public let title: String? public let desc: String? @@ -41,8 +40,7 @@ public struct Tweak { title: String? = nil, description: String? = nil, group: String? = nil, - source: String? = nil, - decryptedValue: TweakValue? = nil) { + source: String? = nil) { self.feature = feature self.variable = variable self.value = value @@ -50,7 +48,6 @@ public struct Tweak { self.desc = description self.group = group self.source = source - self.decryptedValue = decryptedValue } func mutatedCopy(feature: String? = nil, @@ -59,16 +56,14 @@ public struct Tweak { title: String? = nil, description: String? = nil, group: String? = nil, - source: String? = nil, - decryptedValue: TweakValue? = nil) -> Self { + source: String? = nil) -> Self { Self(feature: feature ?? self.feature, variable: variable ?? self.variable, value: value ?? self.value, title: title ?? self.title, description: description ?? self.desc, group: group ?? self.group, - source: source ?? self.source, - decryptedValue: decryptedValue ?? self.decryptedValue) + source: source ?? self.source) } } diff --git a/JustTweak/Classes/TweakManager.swift b/JustTweak/Classes/TweakManager.swift index 5b13219..aedf024 100644 --- a/JustTweak/Classes/TweakManager.swift +++ b/JustTweak/Classes/TweakManager.swift @@ -93,19 +93,17 @@ extension TweakManager: MutableTweakProvider { if let tweak = tweakProvider.tweakWith(feature: feature, variable: variable) { logClosure?("Tweak '\(tweak)' found in configuration \(tweakProvider))", .verbose) - let tweakWithoutDecryptedValue = Tweak(feature: feature, - variable: variable, - value: tweak.value, - title: tweak.title, - group: tweak.group, - source: "\(type(of: tweakProvider))") - - let decryptedValue = tweakProvider.decryptionClosure?(tweakWithoutDecryptedValue) - result = tweakWithoutDecryptedValue.mutatedCopy(decryptedValue: decryptedValue) + result = Tweak(feature: feature, + variable: variable, + value: tweak.value, + title: tweak.title, + group: tweak.group, + source: "\(type(of: tweakProvider))") break } else { - logClosure?("Tweak with identifier '\(variable)' NOT found in configuration \(tweakProvider))", .verbose) + let logMessage = "Tweak with identifier '\(variable)' in configuration \(tweakProvider)) could NOT be found or has an invalid configuration" + logClosure?(logMessage, .verbose) } } if let result = result { diff --git a/JustTweak/Classes/TweakProviders/LocalTweakProvider.swift b/JustTweak/Classes/TweakProviders/LocalTweakProvider.swift index 32a27a8..e3e631c 100644 --- a/JustTweak/Classes/TweakProviders/LocalTweakProvider.swift +++ b/JustTweak/Classes/TweakProviders/LocalTweakProvider.swift @@ -8,7 +8,7 @@ import Foundation final public class LocalTweakProvider { private enum EncodingKeys : String { - case Title, Description, Group, Value + case Title, Description, Group, Value, Encrypted } private let configurationFile: [String : [String : [String : AnyObject]]] @@ -66,6 +66,7 @@ extension LocalTweakProvider: TweakProvider { let description = entry[EncodingKeys.Description.rawValue] as? String let group = entry[EncodingKeys.Group.rawValue] as? String let value = tweakValueFromJSONObject(entry[EncodingKeys.Value.rawValue]) + let isEncrypted = (entry[EncodingKeys.Encrypted.rawValue] as? Bool) ?? false let tweak = Tweak(feature: feature, variable: variable, @@ -74,6 +75,16 @@ extension LocalTweakProvider: TweakProvider { description: description, group: group) - return tweak.mutatedCopy(decryptedValue: decryptionClosure?(tweak)) + if isEncrypted { + guard let decryptionClosure = decryptionClosure else { + // The configuration is not correct, it's encrypted, but there's no way to decrypt + // So return nil to indicate an error. Should be changed to a throwing function in the future + return nil + } + + return tweak.mutatedCopy(value: decryptionClosure(tweak)) + } else { + return tweak + } } } diff --git a/JustTweak/Classes/TweakProviders/UserDefaultsTweakProvider.swift b/JustTweak/Classes/TweakProviders/UserDefaultsTweakProvider.swift index fd2edcb..85453a3 100644 --- a/JustTweak/Classes/TweakProviders/UserDefaultsTweakProvider.swift +++ b/JustTweak/Classes/TweakProviders/UserDefaultsTweakProvider.swift @@ -31,15 +31,13 @@ extension UserDefaultsTweakProvider: TweakProvider { let userDefaultsValue = userDefaults.object(forKey: userDefaultsKey) as AnyObject? guard let value = updateUserDefaults(userDefaultsValue) else { return nil } - let tweak = Tweak( + return Tweak( feature: feature, variable: variable, value: value, title: nil, group: nil ) - - return tweak.mutatedCopy(decryptedValue: decryptionClosure?(tweak)) } } diff --git a/JustTweak/Classes/UI/TweakManager+Presentation.swift b/JustTweak/Classes/UI/TweakManager+Presentation.swift index 6175158..c069984 100644 --- a/JustTweak/Classes/UI/TweakManager+Presentation.swift +++ b/JustTweak/Classes/UI/TweakManager+Presentation.swift @@ -21,16 +21,8 @@ extension TweakManager { description: jsonTweak.desc, group: jsonTweak.group) - let decryptedTweakValue = decryptionClosure?(aggregatedTweak) - let key = "\(feature)-\(variable)" - tweaks[key] = Tweak(feature: feature, - variable: variable, - value: tweak.value, - title: jsonTweak.title, - description: jsonTweak.desc, - group: jsonTweak.group, - decryptedValue: decryptedTweakValue) + tweaks[key] = aggregatedTweak } } } diff --git a/README.md b/README.md index 0ff0bff..f7b2b5f 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,31 @@ JustTweak comes with three tweak providers out-of-the-box: In addition, JustTweak defines `TweakProvider` and `MutableTweakProvider` protocols you can implement to create your own tweak provider to fit your needs. In the example project you can find some examples which you can use as a starting point. +### Encryption or pre-processing (Advanced) + +JustTweak offers the ability to add a `decryptionClosure` to a `TweakProvider`. This closure takes the `Tweak` as input and returns a `TweakValue` as output. The closure allows you to do some preprocessing on your tweak which can e.g. be used to decrypt values. This can be used if you have an encrypted value in your tweaks JSON file as can be seen below: + +```json +"encrypted_answer_to_the_universe": { + "Title": "Encrypted definitive answer", + "Description": "Encrypted answer to the Ultimate Question of Life, the Universe, and Everything", + "Group": "General", + "Value": "24 ton yletinifeD", + "GeneratedPropertyName": "definitiveAnswerEncrypted", + "Encrypted": true +} +``` + +Note that you have to specify if the value is encrypted in your JSON file (with the `Encrypted` property) for the decryption closure to process the value. The decryption closure for the JSON above can be specified as follows: + +```swift +tweakProvider.decryptionClosure = { tweak in + // decrypt `tweak.value` with your cypher of choice and return the decrypted value +} +``` + +In this way, the tweak fetched from the tweak provider will have the decrypted value. + ## License JustTweak is available under the Apache 2.0 license. See the LICENSE file for more info. diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/Tweaks.json b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/Tweaks.json index 2aea73a..0a03fbc 100644 --- a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/Tweaks.json +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/Tweaks.json @@ -32,6 +32,14 @@ } }, "general": { + "encrypted_answer_to_the_universe": { + "Title": "Encrypted definitive answer", + "Description": "Encrypted answer to the Ultimate Question of Life, the Universe, and Everything", + "Group": "General", + "Value": "24 ton yletinifeD", + "GeneratedPropertyName": "definitiveAnswerEncrypted", + "Encrypted": true + }, "answer_to_the_universe": { "Title": "Definitive answer", "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything", diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/TweakLoaderTests.swift b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/TweakLoaderTests.swift index cc890be..cf38ebf 100644 --- a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/TweakLoaderTests.swift +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/TweakLoaderTests.swift @@ -34,6 +34,13 @@ class TweakLoaderTests: XCTestCase { group: "General", valueType: "Int", propertyName: "definitiveAnswer"), + Tweak(feature: "general", + variable: "encrypted_answer_to_the_universe", + title: "Encrypted definitive answer", + description: "Encrypted answer to the Ultimate Question of Life, the Universe, and Everything", + group: "General", + valueType: "String", + propertyName: "definitiveAnswerEncrypted"), Tweak(feature: "general", variable: "greet_on_app_did_become_active", title: "Greet on app launch",