Locksmith icon indicating copy to clipboard operation
Locksmith copied to clipboard

Peculiar disconnect between storing and reading data

Open skington opened this issue 10 years ago • 3 comments

Caveat: I'm normally a Perl programmer and I'm new to Swift. So many of the assertions here may be wrong. OTOH, this means this is a new / outside opinion you might not have heard of!

If I have an object that implements CreateableSecureStorable, I just have to say e.g.

class Thing: ... {
    var data: [String: AnyObject] {
        return ["username": username!, "password": password!]
    }
    // ...
    func store() {
        self.createInSecureStore()
    }
}

and all of this will be fished out of my object for me.

If I want to read all of this again, though, I believe I have to say e.g.

    func fetch() -> Thing? {
        guard let result = self.readFromSecureStore() else { return nil }
        var newThing = Thing()
        newThing.username = result?.username
        newThing.password = result?.password
        return newThing
    }

If I have more than a trivial amount of things to store this is going to get tiresome very quickly.

Now, if I define data as having a setter I believe I can say this instead:

    var data: [String: AnyObject] {
        get {
            return ["username": username!, "password": password!]
        }
        set(keychainData) {
            self.username = keychainData["username"] as? String
            self.password = keychainData["password"] as? String
        }
    }
    func fetch() -> Thing? {
        guard let result = self.readFromSecureStore() else { return nil }
        let newThing = Thing()
        newThing.data = result.data!
        return newThing
    }

This is at least somewhat better: the two bits of code that convert between the dictionary storage and the object's internal representation are now right beside each other, rather than split between two different parts of the source code.

But it still involves a fair amount of cut and paste.

Ideally I should be able to say e.g. (this is now bordering on pseudo-code)

    func mapping() -> [String: AnyObject] {
        return [
            "username": &username,
            "password": &password,
        ]
    }

and all of the serialisation / deserialisation code would know what to pick out of / stuff into a data structure like this. But I image this would thwart a lot of the Swift type-strictness.

skington avatar Jan 28 '16 03:01 skington

I spent some time a while back on this in the Alamofire project (an http manager). The struct-to-dictionary conversion could be done trivially using Mirror, but afaik there is no way to set a struct's property given a string of its name. This is possible with classes vs. structs on iOS/Mac using the Objective-C runtime, but that wouldn't work here.

I'd love to be corrected on this if I'm wrong.

getaaron avatar Jan 28 '16 05:01 getaaron

Really interesting points raised. Will have to do a bit more reading on Mirror too—looks useful.

I don't know if it would actually work in the end, or if it solves the problem at hand, but I've been thinking that generics might be able to help us out with this problem.

This does just move the problem of the duplicated serialize/deserialize code to ThingCredentials, but that might be helpful since the user then gets to specify how ThingCredentials works

protocol Serializable {
  // (note [String: AnyObject] probably wouldn't end up being the type... probably NSData or something lower)
  func serialize() -> [String: AnyObject]
  init(sourceData: [String: AnyObject])
}

protocol CreateableSecureStorable {
  typealias DataType: Serializable

  var data: DataType
}

struct ThingCredentials: Serializable {
  let token: String

  init(token: String) {
    self.token = token
  }

  init(sourceData: [String: AnyObject]) {
     self.token = sourceData["token"]
  }

  func serialize() -> [String: AnyObject] {
    return ["token": token]
  }
}

struct Thing: CreateableSecureStorable, Readable {
  typealias DataType = ThingCredentials

  init?(credentials: ThingCredentials) {
    // However you'd init with credentials
  }

  func fetch() -> Thing? { // (depending on changes to how reading works)
    let result = self.readFromSecureStore // Constructs a result type with data set to the result of init(sourceData:)
    let data = result.data // type is ThingCredentials
    return self.init(credentials: data) // (or maybe you add a method to ThingCredentials to construct a Thing)
  }
}

matthewpalmer avatar Jan 28 '16 21:01 matthewpalmer

I have it done this way:

protocol MyServiceCredentialsKeychainAccount: GenericPasswordSecureStorable {
}

extension MyServiceCredentialsKeychainAccount {
    var service: String { return "MyService" }
    var account: String { return "rehsals" }
}

struct MyServiceCredentials: CreateableSecureStorable, DeleteableSecureStorable, MyServiceCredentialsKeychainAccount {
    let password: String

    var data: [String: AnyObject] {
        get {
            return ["password": password]
        }
    }

    static var credentials: MyServiceCredentials? {
        let deserializator = CospaceCredentialsDeserializator()
        return deserializator.deserialize()
    }
}

struct MyServiceCredentialsDeserializator: ReadableSecureStorable, MyServiceCredentialsKeychainAccount {
    func deserialize() -> MyServiceCredentials? {
        guard let resultData = readFromSecureStore()?.data,
            let password = resultData["password"] as? String else {
                return nil
        }
        return MyServiceCredentials(password: password)
    }
}

rehsals avatar Sep 08 '16 10:09 rehsals