0

Xcode 10.2 and Swift 5

I pick an image from camera and calculate SHA256 hash of the image as string and save the image to iPhone photo album.

//Save photo to album. Photo comes from UIImagePickerController
UIImageWriteToSavedPhotosAlbum(self.photo, self, #selector(saveimage(_:didFinishSavingWithError:contextInfo:)), nil)

//Calculate hash
let imageData : Data = self.photo!.pngData()! 
let imageHash : String = getImageHash(data: imageData)    

func getImageHash(data : Data) -> String {

    var hashBytes = [UInt8](repeating: 0,  count: Int(CC_SHA256_DIGEST_LENGTH))
    data.withUnsafeBytes {
        _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hashBytes)
    }

    var hex = ""
    for index in 0..<Int(CC_SHA256_DIGEST_LENGTH) {
        hex += String(format: "%02x", hashBytes[index])
    }
    print(hex)
    return hex
}

The code, however, gives me a different SHA256 of the image than after it has been saved. I transferred the photo to my Mac and checked hash by shasum -a 256 as well as uploaded it directly from iPhone to an online hash generator which gave me same hash as Mac did.

So, either my code to calculate the hash is wrong or something is being changed while storing the photo such as name or properties using UIImageWriteToSavedPhotosAlbum(...). Any ideas how to fix this?

nullpointr
  • 524
  • 4
  • 18
  • 1
    PNG compresses some data. Maybe that causes the difference in the hash. Another option might be that the PNG data isn't the same as the actual file data. This would explain why both the Mac and the online generator worked, but the Swift version didn't. I would suggest loading the actual image file into a `Data` object and try again. – Bram Apr 26 '19 at 18:11
  • @Craz1k0ek Thanks, after all, I have to compress the actual image via pngData to get the data, or am I wrong? – nullpointr Apr 26 '19 at 21:21
  • I’m not sure if you can get the image file data indeed. What you could try is to have the base64 encoded string printed via `imageData.base64EncodedString()`. Then try to hash that data at an alternative platform. – Bram Apr 26 '19 at 23:11

1 Answers1

3

I have done some further research on the matter and it appears that the data you have inside the UIImage is indeed different from the data that you open as a file, and I believe that is exactly what causes the problem. Note that I have shortened the base64 data, for readability.

Swift

func getImageHash(data: Data) -> String {
    var hashBytes = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)
    data.withUnsafeBytes {
        _ = CC_SHA256($0.baseaddress, CC_LONG(data.count), &hashBytes)
    }
    var hex = ""
    for index in 0 ..< Int(CC_SHA256_DIGEST_LENGTH) {
        hex += String(format: "%02x", hashbytes[index])
    }
    return hex
}

The function is fine, I'll use an example using the assets folder of iOS.

let imageData = UIImage(named: "Example")!.pngData()!
print(imageData.base64EncodedString())
// 'iVBORw0KGgoAAAANSUhEUgAAAG8AAACACAQAAACv3v+8AAAM82lD [...] gAAAABJRU5ErkJggg=='
let imageHash = getImageHash(data: imageData)
print(imageHash)
// '145036245c9f675963cc8de2147887f9feded5813b0539d2320d201d9ce63397'

The hash is calculated correctly as far as we are concerned right now. I was interested in the base64 data, so I can use that on other platforms too.

Python

import hashlib
import base64

img_d = base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAG8AAACACAQAAACv3v+8AAAM82lD [...] gAAAABJRU5ErkJggg==')
m = hashlib.sha256()
m.update(img_d)
m.digest().hex()
# '145036245c9f675963cc8de2147887f9feded5813b0539d2320d201d9ce63397'

So it appears the hash is calculated correctly, but only for the base64 data. So what is the difference? Let's use Python again to investigate. I used PIL to load the image directly.

import hashlib
from PIL import Image

i = Image.open('/path/to/file')
img_d = i.tobytes()
m = hashlib.sha256()
m.update(img_d)
m.digest().hex()
# 'f650b1b95a50c3a2b77da7a0825c3c01066385a12c8fe50b449ffc8c7249e370'

So now we have, indeed, a different hash. Let's try one last thing, openssl dgst in the terminal.

Terminal

echo 'iVBORw0KGgoAAAANSUhEUgAAAG8AAACACAQAAACv3v+8AAAM82lD [...] gAAAABJRU5ErkJggg==' | base64 --decode | openssl dgst -sha256
145036245c9f675963cc8de2147887f9feded5813b0539d2320d201d9ce63397

Alright, so indeed, openssl digest can calculate the hash over the base64 data and it does indeed match.

openssl dgst -sha256 /path/to/file
SHA256(/path/to/file)= d15c2d0c186a46b41c34a312eaf1c7e4b0f7a51bdb9c53a91dc361385ba23e64

So it's very interesting. The base64 data hashed has a 100% success rate, but loading it in Python and in the terminal, resulted in two different hashes. I do not know the reason why even they are different. I do believe what I mentioned earlier in the comments is correct, hashing a file will result in a different hash, as the meta data of the file is hashed too.

In order to provide a solution: try to hash your raw image data, instead of the file. This has the biggest chance of success. Hope this helps.

Bram
  • 2,718
  • 1
  • 22
  • 43
  • 1
    Awesome! Thanks for your work. I will try to reproduce your steps. I already tried this solution https://stackoverflow.com/a/50931949/1419276 which does not use pngDate function, instead it converts image to cgimage/dataprovider, but still having the problem of a different hash. I will investigate this further. – nullpointr Apr 29 '19 at 09:45