Swift NSKeyedUnarchiver Failing After Converting from String to Data - swift

I have a class that conforms to NSCoding. I want to convert that class into data, then into a string that can be stored on backend.
The issue is that once it's a converted from a String back to Data it fails to unarchive properly with NSKeyedUnarchiver, so converting to a string must be corrupting the data.
Here's the code:
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: drawItems, requiringSecureCoding: false)
let stringData = String(decoding: data, as: UTF8.self)
print("HERE successfully converted to a string: ", stringData)
let stringBackToData: Data? = stringData.data(using: .utf8)
if let stringBackToData = stringBackToData, let allItems = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(stringBackToData) as? [DrawItem] {
print("HERE items: ", allItems)
} else {
print("HERE FAILED to unarchive")
}
catch {
print("Failed: ", error)
}
What is wrong with this string conversion?

The default outputFormat for NSKeyedArchiver is .binary, which is not convertible to or from UTF-8. In your example, String(decoding: data, as: UTF8.self) "repairs" invalid UTF-8 bytes by replacing them with the UTF-8 replacement character (0xFFFD).
This corrupts the archiver data, and prevents it from decoding properly.
If you really can't transmit binary data to your backend as-is, or store it that way, and must convert the data into a string, you have two options:
NSKeyedArchiver has an alternate .outputFormat of .xml, which produces UTF-8 XML data. You can produce this data by creating an archiver and configuring it:
let archiver = NSKeyedArchiver(requiringSecureCoding: false)
archiver.outputFormat = .xml
archiver.encode(drawItems, forKey: NSKeyedArchiveRootObjectKey)
if let error = archiver.error {
// Handle the error. This is the same error that would be produced
// by `NSKeyedArchiver.archivedData(withRootObject:requiringSecureCoding:)`
} else {
let data = archiver.encodedData
// `data` is UTF-8 XML data ready to be transmitted/stored
}
Alternatively, you can continue using the convenience method you are now, but Base64 encode it:
let data = try NSKeyedArchiver.archivedData(withRootObject: drawItems, requiringSecureCoding: false)
let base64EncodedData = data.base64EncodedString()
Which you use is up to you: on its own, the base64-encoded string is shorter than the raw XML data (because the starting binary data is significantly smaller than its equivalent XML), but the XML data does compress significantly better than the base64 data does, and may very well end up smaller. You can test both approaches to see what fits your use-case better.

Related

Issue with UserDefaults (converting data to array and back)

What I want to do:
I want to get an array from UserDefaults that I saved beforehand and append a custom object to it. Afterwards I want to encode it as a Data-type again and set this as the UserDefaults Key again.
My problem:
The encoding part is what is not working as intended for me.
It says: -[__SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x60000011a540
But I do not know how to fix this.
Below is my code for more context:
do {
let decoded = defaults.object(forKey: "ExArray") as! Data
var exo = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decoded) as! [Exerc]
exo.append(datas[indexPath.row])
let enco = try NSKeyedArchiver.archivedData(withRootObject: exo, requiringSecureCoding: false) <- Here is the error
defaults.set(enco, forKey: "ExArray")
} catch {
print("Error encoding custom object NOSEARCHO")
}
This is how Exerc looks:
struct Exerc: Codable {
var title: String
var exID: String
}
Seems like you are not using the archiver features, so why don't you just use the codable?
do {
let key = "ExArray"
let decoded = defaults.data(forKey: key)!
var exo = try JSONDecoder().decode([Exerc].self, from: decoded)
exo.append(datas[indexPath.row])
let enco = try JSONEncoder().encode(exo)
defaults.set(enco, forKey: key)
} catch {
print("Error encoding/decoding custom object NOSEARCHO", error)
}
It just a simple refactored MVP of the original code, but you can even work a bit on this and make it human readable right in the plist file!

What does this base64 related function do [in Swift]?

Can someone explain the behavior of the function below? Some have suggested to not use NSData. Do you have better alternatives to mention? If the returned value is Base64Encoded can I decode on one of the online encoders/decoders? Thanks.
func stringToData(message: String) -> NSData? {
let strData = NSData(base64Encoded: message, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters)
return strData
}
NSData(base64Encoded:options:) is documented to attempt to initialize a data object with the given Base64 encoded string—and return nil if it fails. In other words; it decodes a Base64 encoded string as an NSData object.
In Swift, you would likely use the base64EncodedString() function and the Data(base64Encoded:) initializer on the Data type to encode and decode data as Base64 strings, for example like this:
let originalData = Data(bytes: [1,2,3,4,5,6,7,8,9,10,11,12])
let encodedAsBase64String = originalData.base64EncodedString()
// "AQIDBAUGBwgJCgsM"
let decodedData = Data(base64Encoded: encodedAsBase64String) // is optional because the decoding can fail
// 12 bytes: <01020304 05060708 090A0B0C>

data from base64 url

I have a URL in the form of
foo://?data:application/x-foo;base64,OjAyMDAwMDA0MDAwMEZBDQo6MTAwMDA...
and now need to extract the base64 data into a Data object.
Unfortunately it seems the Data object does not support this yet as
let data = try Data(contentsOf: url)
returns NSURLConnection finished with error - code -1002 when trying.
While I could decode the URL manually I am wondering if I am missing a simple standard way of doing this. How would you do this?
Actually you can decode Base64 data from an URL (see for
example Base64 Decoding in iOS 7+ where this is demonstrated in Objective-C). The format is a bit different from what
you have:
let url = URL(string: "data:application/octet-stream;base64,SGVsbG8gd29ybGQh")!
let data = try! Data(contentsOf: url)
print(String(data: data, encoding: .utf8)!) // Hello world!
(Error checking omitted for brevity.)
You have to separate the base64 encoded part of the URL from the other parts, decode it, then join the original non-encoded part with the decoded part and get the data from there.
extension URL {
init?(partialBase64Url: String){
guard let base64part = base64Url.components(separatedBy: "base64,").last, let base64Data = Data(base64Encoded: base64part), let decodedString = String(data: base64Data, encoding: .utf8) else {
return nil
}
let decodedUrl = base64Url.components(separatedBy: "base64,").dropLast().joined() + decodedString
self.init(string: decodedUrl)
}
}
let decodedUrl = URL(partialBase64Url: "foo://?data:application/x-foo;base64,dGVzdFVybFN0cmluZw==")
Value of decodedUrl: "foo://?data:application/x-foo;testUrlString", as expected, since dGVzdFVybFN0cmluZw== is the base64 encoded value of testUrlString.

Different console output data via NSData and Data (Xcode 8 beta 6, Swift 3)

I write a little snippet of usual code but found that my code don't return hex data from server with this line of code:
let currentData = try! Data(contentsOf: fullURL!)
print("currentData=", currentData)
And the output:
currentData= 24419 bytes
I tried to use Leo's comment link:
stackoverflow.com/q/39075043/2303865
I got something hex data without spaces, and validator (http://jsonprettyprint.com) can't recognise it and returns null.
Let's try to sort out the different issues here and summarize the
above comments.
The description method
of Data prints only a short summary "NNN bytes", and not a hex dump
as NSData did:
let o = ["foo": "bar"]
let jsonData = try! JSONSerialization.data(withJSONObject: o)
print(jsonData) // 13 bytes
You can get a hex dump by bridging to NSData (source):
print(jsonData as NSData) // <7b22666f 6f223a22 62617222 7d>
or by writing an extension method for Data (How to convert Data to hex string in swift).
But that is actually not the real problem. The JSON validator needs
the JSON as a string, not as a hex dump (source):
print(String(data: jsonData, encoding: .utf8)!) // {"foo":"bar"}
And to de-serialize the JSON data into an object you would need
none of the above and just call
let obj = try JSONSerialization.jsonObject(with: jsonData)

swift aes 128 decryption with string input

I'm using zaph's example for encryption and decryption from this post
The encryption works well, and with my encryption key and iv, returns an NSData object, containing the following string: "bc6983a8 65d412df 2bafdc40 f569874e", which is my input text encrypted. The content of the returned NSData object:
encrypted text: <bc6983a8 65d412df 2bafdc40 f569874e>
This text is sent to a server (json), and the server returns a response, also encrypted with the same encryption key and iv.
My question is, how can I convert the string text that comes from the server's response(bc6983a8 65d412df 2bafdc40 f569874e, for example) into an NSData object so that i can decrypt it?
I tried the follwing:
let plainData = ("<bc6983a8 65d412df 2bafdc40 f569874e>" as NSString).dataUsingEncoding(NSUTF8StringEncoding)!;
let plainData = ("<bc6983a8 65d412df 2bafdc40 f569874e>" as NSString).dataUsingEncoding(NSUTF8StringEncoding)!;
let base64String = plainData.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0));
let dataDec = NSData(base64EncodedString: base64String, options: NSDataBase64DecodingOptions(rawValue: 0))
But when displaying the contents of the NSData object, the output is not the one expected:
data Optional("<3c626336 39383361 38203635 64343132 64662032 62616664 63343020 66353639 38373465 3e>")
Any help is appreciated.
You are converting your NSData to the string in a wrong way. Follow this code to convert NSData to string
//This is your encrypted data
var encryptedData = NSData()
let plainData = encryptedData(data: encryptedData, encoding: NSUTF8StringEncoding)
Hope this will work for you.
UPDATE:
This happens because you are not correctly fetching the string from your backend. Use proper method for decoding json data instead of just printing it. "<bc6983a8 65d412df 2bafdc40 f569874e>" is not the string you actually need. You need to decode your json data
See the sample code
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if error != nil
{
print("error=\(error)", terminator: "")
return
}
do{ if let newdata = try? (NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments) as! NSDictionary)
{
print(newdata)
}
Here 'newdata' may include the encrypted string you need. Parse it from that json, convert it to NSData and then decrypt.
UPDATE 2
Use this code to convert your data to string
let resstr = NSString(data: YourData, encoding: NSUTF8StringEncoding)