Closed JarnoRFB closed 2 years ago
Hi @JarnoRFB
Thank you for raising your feature request. This is definitely something on our radar, but I can't share any concrete timelines. Is this something you urgently need?
Regards, Yurii Adyen
@kaphacius thanks for the update! Yes we would need this rather urgently, but for now we would attempt to do this ourself by following https://docs.adyen.com/point-of-sale/choose-your-architecture/local/protect and translating the code to swift, if nothing else is availble right now.
@JarnoRFB I do have some bits of the implementation already, but it is not yet ready to be open-sourced. If you have any concrete questions - I might be able to answer them.
@kaphacius Glad about any bits that you can share already. I am happy to provide feedback once the implementation is working on our side.
@JarnoRFB alright, this is what I have for you right now. have you been able extract the hmac key data, cipher key data and the static initialization vector?
/// Encrypts the payload
///
/// - Parameter payload: The payload data to be encrypted
/// - Parameter iv: The initialization vector
/// - Throws: `CipherAlgorithmError.encryptionFailure`
/// - Returns: The cipher data.
func encrypt(_ payload: Data, iv: Data) throws -> Data {
let dataOutLength = payload.count + kCCBlockSizeAES128
guard let dataOut = NSMutableData(length: dataOutLength) else {
throw CipherAlgorithmError.encryptionFailure
}
let operation = CCOperation(kCCEncrypt)
return try aesCrypt(
operation: operation,
keyData: key.data,
iv: iv,
dataIn: payload,
dataOut: dataOut
)
}
/// Decrypts the cipher data.
///
/// - Parameter cipherText: The cipher data to be decrypted
/// - Parameter iv: The initialization vector
/// - Throws: `CipherAlgorithmError.decryptionFailure`
/// - Returns: The plain decrypted data.
func decrypt(_ cipherText: Data, iv: Data) throws -> Data {
let dataOutLength = cipherText.count
let dataIn = cipherText.subdata(in: 0..<dataOutLength)
guard let dataOut = NSMutableData(length: dataOutLength) else {
throw CipherAlgorithmError.decryptionFailure
}
let operation = CCOperation(kCCDecrypt)
return try aesCrypt(
operation: operation,
keyData: key.data,
iv: iv,
dataIn: dataIn,
dataOut: dataOut
)
}
/// Executes a cryptographic operation
/// - Parameters:
/// - operation: The operation to execute
/// - keyData: The key data
/// - iv: The initialization vector
/// - dataIn: Data to execute the operation with
/// - dataOut: Data indicating the expected length of the operation result
/// - Throws: CipherAlgorithmError.operationFailure if the operation fails
/// - Returns: The result of the operation
internal func aesCrypt(
operation: CCOperation,
keyData: Data,
iv: Data,
dataIn: Data,
dataOut: NSMutableData
) throws -> Data {
let keyData = keyData as NSData
let dataIn = dataIn as NSData
let iv = iv as NSData
var numBytesOut: size_t = 0
let algorithm = CCAlgorithm(kCCAlgorithmAES)
let options = CCOptions(kCCOptionPKCS7Padding)
let status: CCCryptorStatus = CCCrypt(
operation,
algorithm,
options,
keyData.bytes,
keyData.length,
iv.bytes,
dataIn.bytes,
dataIn.length,
dataOut.mutableBytes,
dataOut.length,
&numBytesOut
)
guard status == kCCSuccess else {
// Handle error
throw CipherAlgorithmError.operationFailure("\(operation)")
}
return (dataOut as Data)[0..<numBytesOut]
}
@kaphacius thanks for sharing. Here is what we have so far. We are using a combination of CryptoSwift and CommonCrypto. Both work for deriving the key material, but no idea if we use it in the right way.
Also the sample code is a bit convoluted now, as it handles the encryption and sends the response in one go, but it is good for testing for now.
Currently we just get an empty response with a status of 400 back from the terminal server, which seems a bit weird, given that the got Nexo Service: crypto error
before, when the encryption had other bugs.
Haven't tried out your code for encryption until now though.
import AdyenNetworking
import CommonCrypto
import CryptoSwift
import Foundation
class LocalNexoConnection {
static let hmacLength = 32
static let cipherLength = 32
static let ivLength = 16
let session: URLSession
init(session: URLSession) {
self.session = session
}
func send() {
Logging.isEnabled = true
let poiId = "V400cPlus-..."
let saleId = "visiolab-sample-app"
let password = "..."
let salt = "AdyenNexoV1Salt"
let keyLength = Self.hmacLength + Self.cipherLength + Self.ivLength
print("key length \(keyLength)")
do {
let key = pbkdf2SHA1(password: password, salt: salt.data(using: .utf8)!, keyByteCount: keyLength, rounds: 4000)!.bytes
print("key generated \(key)")
// let key2 = try PKCS5.PBKDF2(
// password: Array(password.utf8),
// salt: Array(salt.utf8), iterations: 4000, keyLength: 80, variant: .sha1)
// .calculate()
// print("key2 generated \(key)")
let hmacKey = Array(key[0 ..< Self.hmacLength])
let cipherKey = Array(key[Self.hmacLength ..< Self.hmacLength + Self.cipherLength])
let iv = Array(key[Self.hmacLength + Self.cipherLength ..< Self.hmacLength + Self.cipherLength + Self.ivLength])
var actualIv: [UInt8] = []
let nonce = AES.randomIV(Self.ivLength)
for (x1, x2) in zip(iv, nonce) {
actualIv.append(x1 ^ x2)
}
let aes = try AES(key: cipherKey, blockMode: CBC(iv: actualIv), padding: .pkcs7)
let myrequest = SaleToPOIRequest(
messageHeader: MessageHeader(
ServiceID: String(UUID().uuidString.prefix(7)), SaleID: saleId, POIID: poiId
),
paymentRequest: PaymentRequest(
saleData: SaleData(
saleTransaction: SaleTransaction(
transactionId: String(UUID().uuidString.prefix(7)),
timestamp: Date().ISO8601Format()
)),
paymentTransaction: PaymentTransaction(
amountsReq: AmountsReq(
currency: "EUR",
requestedAmount: 5
))
)
)
let jsonData = try JSONEncoder().encode(myrequest)
print("json request", String(data: jsonData, encoding: .utf8)!)
let nexoBlob = try aes.encrypt(jsonData.bytes)
let hmac = hashHmac(message: jsonData, key: Data(hmacKey))
let encryptedRequest = EncrytedSaleToPOIRequest(
MessageHeader: myrequest.messageHeader,
NexoBlob: nexoBlob.toBase64(),
SecurityTrailer: SecurityTrailer(
KeyVersion: 1,
KeyIdentifier: "...",
Hmac: hmac.bytes.toBase64(),
Nonce: nonce.toBase64(),
AdyenCryptoVersion: 1
)
)
let Url = String(format: "https://192.168.0.105:8443/nexo")
guard let serviceUrl = URL(string: Url) else { return }
let parameterDictionary = ["SaleToPOIRequest": encryptedRequest]
var request = URLRequest(url: serviceUrl)
request.httpMethod = "POST"
request.setValue("Application/json", forHTTPHeaderField: "Content-Type")
guard let httpBody = try? JSONEncoder().encode(parameterDictionary) else {
return
}
print("httpBody \(String(data: httpBody, encoding: .utf8)!)")
request.httpBody = httpBody
session.dataTask(with: request) { data, response, error in
if let response = response {
print("response", response)
}
if let data = data {
print("reponse data", String(data: data, encoding: .utf8))
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
print(json)
} catch {
print("json error", error)
}
}
if let error = error {
print("error", error)
}
}.resume()
} catch {
print(error)
}
}
}
func pbkdf2SHA1(password: String, salt: Data, keyByteCount: Int, rounds: Int) -> Data? {
return pbkdf2(hash: CCPBKDFAlgorithm(kCCPRFHmacAlgSHA1), password: password, salt: salt, keyByteCount: keyByteCount, rounds: rounds)
}
func pbkdf2(hash: CCPBKDFAlgorithm, password: String, salt: Data, keyByteCount: Int, rounds: Int) -> Data? {
let passwordData = password.data(using: String.Encoding.utf8)!
var derivedKeyData = Data(repeating: 0, count: keyByteCount)
let count = derivedKeyData.count
let derivationStatus = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes in
salt.withUnsafeBytes { saltBytes in
CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
password, passwordData.count,
saltBytes, salt.count,
hash,
UInt32(rounds),
derivedKeyBytes, count
)
}
}
if derivationStatus != 0 {
print("Error: \(derivationStatus)")
return nil
}
return derivedKeyData
}
func hashHmac(message: Data, key: Data) -> Data {
let hashAlgorithm = kCCHmacAlgSHA256
let length = CC_SHA256_DIGEST_LENGTH
var macData = Data(count: Int(length))
macData.withUnsafeMutableBytes { macBytes in
message.withUnsafeBytes { messageBytes in
key.withUnsafeBytes { keyBytes in
CCHmac(CCHmacAlgorithm(hashAlgorithm),
keyBytes, key.count,
messageBytes, message.count,
macBytes)
}
}
}
return macData
}
Just found the error. Just the SaleToPOI
field was missing from the message.
The following is working now
import AdyenNetworking
import CommonCrypto
import CryptoSwift
import Foundation
class LocalNexoConnection {
static let hmacLength = 32
static let cipherLength = 32
static let ivLength = 16
let session: URLSession
init(session: URLSession) {
self.session = session
}
func send() {
Logging.isEnabled = true
let poiId = "V400cPlus-..."
let saleId = "visiolab-sample-app"
let password = "..."
let salt = "AdyenNexoV1Salt"
let keyLength = Self.hmacLength + Self.cipherLength + Self.ivLength
print("key length \(keyLength)")
do {
let key = pbkdf2SHA1(password: password, salt: salt.data(using: .utf8)!, keyByteCount: keyLength, rounds: 4000)!.bytes
// print("key2 generated \(Array(key!))")
// let key = try PKCS5.PBKDF2(
// password: Array(password.utf8),
// salt: Array(salt.utf8), iterations: 4000, keyLength: 80, variant: .sha1)
// .calculate()
print("key generated \(key)")
let hmacKey = Array(key[0 ..< Self.hmacLength])
let cipherKey = Array(key[Self.hmacLength ..< Self.hmacLength + Self.cipherLength])
let iv = Array(key[Self.hmacLength + Self.cipherLength ..< Self.hmacLength + Self.cipherLength + Self.ivLength])
print(hmacKey)
var actualIv: [UInt8] = []
let nonce = AES.randomIV(Self.ivLength)
for (x1, x2) in zip(iv, nonce) {
actualIv.append(x1 ^ x2)
}
let aes = try AES(key: cipherKey, blockMode: CBC(iv: actualIv), padding: .pkcs7)
let myrequest = SaleToPOIRequest(
messageHeader: MessageHeader(
ServiceID: String(UUID().uuidString.prefix(7)), SaleID: saleId, POIID: poiId
),
paymentRequest: PaymentRequest(
saleData: SaleData(
saleTransaction: SaleTransaction(
transactionId: String(UUID().uuidString.prefix(7)),
timestamp: Date().ISO8601Format()
)),
paymentTransaction: PaymentTransaction(
amountsReq: AmountsReq(
currency: "EUR",
requestedAmount: 5
))
)
)
let jsonData = try JSONEncoder().encode(["SaleToPOIRequest": myrequest])
print("json request", String(data: jsonData, encoding: .utf8)!)
let nexoBlob = try aes.encrypt(jsonData.bytes)
let hmac = hashHmac(message: jsonData, key: Data(hmacKey))
let encryptedRequest = EncrytedSaleToPOIRequest(
MessageHeader: myrequest.messageHeader,
NexoBlob: nexoBlob.toBase64(),
SecurityTrailer: SecurityTrailer(
KeyVersion: 1,
KeyIdentifier: "...",
Hmac: hmac.bytes.toBase64(),
Nonce: nonce.toBase64(),
AdyenCryptoVersion: 1
)
)
let Url = String(format: "https://192.168.0.105:8443/nexo")
guard let serviceUrl = URL(string: Url) else { return }
let parameterDictionary = ["SaleToPOIRequest": encryptedRequest]
var request = URLRequest(url: serviceUrl)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let httpBody = try? JSONEncoder().encode(parameterDictionary) else {
return
}
print("httpBody \(String(data: httpBody, encoding: .utf8)!)")
request.httpBody = httpBody
session.dataTask(with: request) { data, response, error in
if let response = response {
print("response", response)
}
if let data = data {
print("reponse data", String(data: data, encoding: .utf8))
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
print(json)
} catch {
print("json error", error)
}
}
if let error = error {
print("error", error)
}
}.resume()
} catch {
print(error)
}
}
}
func pbkdf2SHA1(password: String, salt: Data, keyByteCount: Int, rounds: Int) -> Data? {
return pbkdf2(hash: CCPBKDFAlgorithm(kCCPRFHmacAlgSHA1), password: password, salt: salt, keyByteCount: keyByteCount, rounds: rounds)
}
func pbkdf2(hash: CCPBKDFAlgorithm, password: String, salt: Data, keyByteCount: Int, rounds: Int) -> Data? {
let passwordData = password.data(using: String.Encoding.utf8)!
var derivedKeyData = Data(repeating: 0, count: keyByteCount)
let count = derivedKeyData.count
let derivationStatus = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes in
salt.withUnsafeBytes { saltBytes in
CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
password, passwordData.count,
saltBytes, salt.count,
hash,
UInt32(rounds),
derivedKeyBytes, count
)
}
}
if derivationStatus != 0 {
print("Error: \(derivationStatus)")
return nil
}
return derivedKeyData
}
func hashHmac(message: Data, key: Data) -> Data {
let hashAlgorithm = kCCHmacAlgSHA256
let length = CC_SHA256_DIGEST_LENGTH
var macData = Data(count: Int(length))
macData.withUnsafeMutableBytes { macBytes in
message.withUnsafeBytes { messageBytes in
key.withUnsafeBytes { keyBytes in
CCHmac(CCHmacAlgorithm(hashAlgorithm),
keyBytes, key.count,
messageBytes, message.count,
macBytes)
}
}
}
return macData
}
@JarnoRFB happy to hear it works for you, but looks like you already had everything implemented yourself :)
you can use this to make sure it is always wrapped inside SaleToPOIRequest
let paymentRequest = PaymentRequest(...)
let header = MessageHeader(...)
let message = Message(header: header, body: paymentRequest)
try Coder.encode(message)
my idea is to have something like this to create encrypted requests:
struct SecurityTrailer: Codable {
let Hmac: Data
let KeyIdentifier: String
let KeyVersion: Int
let AdyenCryptoVersion: Int
let Nonce: Data
}
struct KeyRepresentation {
let hmacKey: SymmetricKey
let cipherKey: AESKey
let nonceData: Data
init(data: Data) throws {
hmacKey = SymmetricKey(data: data[0..<32])
cipherKey = try AESKey(data: data[32..<64])
nonceData = data[64..<80]
}
let encryptedMessage = EncryptedMessage(message: Message<Body>, keyRepresentation: ...., securityTrailer: ....)
hey @JarnoRFB . turns out majority of the code was already written some time ago. Could you please check the branch local_tapi_encryption_helpers
and see if it works on your side?
Thanks.
@kaphacius quite cool that this moving inside the package! Unfortunately I am currently trapped on an old Mac where I can't use Swift 5.6, but I will try it out as soon as I get a new one. Our custom implementation is working on our side now, but we will be happy to transition to the one provided by you once that is available.
@JarnoRFB not a problem. I lowered the Swift version to 5.5, we were not using anything from the newer versions i believe. would that work for you?
Cool, yes 5.5 should work.
@kaphacius Just tried it out, and it works nicely! The code becomes super sleek. One nice addition would be a utility function that generates the key. Currently I am using
func generateKey(passphrase: String, identifier: String, version: UInt) throws -> EncryptionKey {
guard let key = pbkdf2SHA1(
password: passphrase, salt: "AdyenNexoV1Salt".data(using: .utf8)!, keyByteCount: 80, rounds: 4000
) else { throw NexoError.failedKeyDerivation }
return EncryptionKey(identifier: identifier, version: version, data: key)
}
func pbkdf2SHA1(password: String, salt: Data, keyByteCount: Int, rounds: Int) -> Data? {
return pbkdf2(
hash: CCPBKDFAlgorithm(kCCPRFHmacAlgSHA1),
password: password, salt: salt, keyByteCount: keyByteCount, rounds: rounds
)
}
func pbkdf2(hash: CCPBKDFAlgorithm, password: String, salt: Data, keyByteCount: Int, rounds: Int) -> Data? {
let passwordData = password.data(using: String.Encoding.utf8)!
var derivedKeyData = Data(repeating: 0, count: keyByteCount)
let count = derivedKeyData.count
let derivationStatus = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes in
salt.withUnsafeBytes { saltBytes in
CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
password, passwordData.count,
saltBytes, salt.count,
hash,
UInt32(rounds),
derivedKeyBytes, count
)
}
}
if derivationStatus != 0 {
print("Error: \(derivationStatus)")
return nil
}
return derivedKeyData
}
That works, but could be definitely improved upon.
@JarnoRFB did not mean to close it, i think it closed automatically when the PR got merged. happy to hear it helps and works for you! regarding the keys, this is indeed something we could add. could you please open a separate issue? i'll take a look when i have time
hey @JarnoRFB , we now have the changes you asked for merged into develop
. i mostly used your code, so thanks for that!
@kaphacius Awesome, thanks for letting me know! The terminal integration should be pretty complete with that.
@JarnoRFB i was wondering, how are you doing the certificate validation? are you installing the root certificate on your devices or checking the bundled certificate in the URLSessionDelegate
?
i think that part could still be added, or at least explain in the docs
Is your feature request related to a problem? Please describe. In other SDKs, e.g. node, there exists a layer to abstract the local terminal API including the encryption. Is something similar planned for iOS or are there any guidelines on how to implement the encryption oneself by following https://docs.adyen.com/point-of-sale/choose-your-architecture/local/protect ? Any help is much appreciated.
Describe the solution you'd like An abstraction over the local terminal API that is a easy to use as the node version.