移动安全已成为热门话题。 对于任何远程通信的应用程序,重要的是要考虑通过网络发送的用户信息的安全性。 在本文中,您将学习在Swift中保护iOS应用程序通信的最新最佳实践。
使用HTTPS
开发应用程序时,请考虑将网络请求限制为必不可少的请求。 对于这些请求,请确保它们是通过HTTPS而不是通过HTTP进行的-这将有助于保护您的用户数据免遭“中间人攻击”,其中网络上的另一台计算机充当您的连接的中继,但会监听或更改其传递的数据。 最近几年的趋势是通过HTTPS建立所有连接。 对我们来说幸运的是,新版本的Xcode已经强制执行此操作。
要在iOS上创建简单的HTTPS请求,我们需要做的就是在URL的“ http
”部分附加“ s
”。 只要主机支持HTTPS并具有有效的证书,我们就将获得安全连接。 这适用于URLSession
, NSURLConnection
和CFNetwork等API以及流行的第三方库AFNetworking 。
应用程式传输安全性
多年来,HTTPS对其进行了多次攻击。 由于正确配置HTTPS非常重要,因此Apple已创建了App Transport Security(简称ATS)。 ATS确保您的应用程序的网络连接使用行业标准协议,以确保您不会意外地不安全地发送用户数据。 好消息是,默认情况下,使用当前版本的Xcode构建的应用程序会启用ATS。
ATS自iOS 9和OS X El Capitan起可用。 商店中的当前应用程序不会突然需要ATS,但是默认情况下,针对较新版本的Xcode及其SDK构建的应用程序将启用该功能。 ATS强制执行的一些最佳做法包括使用TLS 1.2版或更高版本,通过ECDHE密钥交换进行前向保密,AES-128加密以及至少使用SHA-2证书。
重要的是要注意,虽然自动启用了ATS,但这并不一定意味着在您的应用程序中会强制实施ATS。 ATS在诸如URLSession
和NSURLConnection
等基础类以及基于流的CFNetwork接口上工作。 在较低级别的网络接口(例如原始套接字,CFNetwork套接字或使用这些较低级别的调用的任何第三方库)上不强制实施ATS。 因此,如果您使用的是底层网络,则必须小心手动实施ATS的最佳实践。
ATS例外
由于ATS强制使用HTTPS和其他安全协议,因此您可能想知道是否仍然能够建立不支持HTTPS的网络连接,例如从CDN缓存下载图像时。 不用担心,您可以在项目的plist文件中控制特定域的ATS设置。 在Xcode中,找到您的info.plist文件,右键单击它,然后选择Open As> Source Code 。
您将找到一个名为NSAppTransportSecurity
的部分。 如果不存在,则可以自己添加代码。 格式如下。
NSAppTransportSecurity
NSExceptionDomains
yourdomain.com
NSIncludesSubdomains
NSThirdPartyExceptionRequiresForwardSecrecy
这使您可以更改所有网络连接的ATS设置。 一些常用设置如下:
-
NSAllowsArbitraryLoads
:禁用ATS。 不要使用这个! Xcode的未来版本将删除此密钥。 -
NSAllowsArbitraryLoadsForMedia
:允许加载不受AV Foundation框架ATS限制的媒体。 如果您的媒体已通过其他方式加密,则仅应允许不安全的加载。 (在iOS 10和macOS 10.12上可用。) -
NSAllowsArbitraryLoadsInWebContent
:可用于关闭应用程序中Web视图对象的ATS限制。 关闭此功能之前,请先考虑一下,因为它允许用户在您的应用程序中加载任意不安全的内容。 (在iOS 10和macOS 10.12上可用。) -
NSAllowsLocalNetworking
:这可用于允许加载本地网络资源而没有ATS限制。 (在iOS 10和macOS 10.12上可用。)
NSExceptionDomains
词典使您可以设置特定域的设置。 以下是可用于您的域的一些有用键的说明:
-
NSExceptionAllowsInsecureHTTPLoads
:允许特定域使用非HTTPS连接。 -
NSIncludesSubdomains
:指定是否将当前规则向下传递到子域。 -
NSExceptionMinimumTLSVersion
:用于指定允许使用的较旧的,安全性较低的TLS版本。
完善的前向保密
虽然加密的流量不可读,但仍可以存储。 如果将来会破坏用于加密该流量的私钥,则可以使用该密钥读取以前存储的所有流量。
为了防止这种妥协,Perfect Forward Secrecy(PFS)会生成一个会话密钥 对于每个通信会话来说都是唯一的。 如果特定会话的密钥被泄露,则不会泄露任何其他会话的数据。 ATS默认情况下实现PFS,您可以使用plist键NSExceptionRequiresForwardSecrecy
来控制此功能。 禁用此选项将允许不支持完美前向保密性的TLS密码。
证书透明度
证书透明性是即将到来的标准,旨在能够检查或审核在HTTPS连接建立期间显示的证书。
当您的主机设置HTTPS证书时,它由所谓的证书颁发机构(CA)颁发。 证书透明性旨在进行近乎实时的监视,以查明证书是恶意发行还是由受感染的证书颁发机构发行。
颁发证书时,证书颁发机构必须将证书提交给多个仅附加证书日志,这些日志以后可以由客户端进行交叉检查,并由域所有者进行审查。 证书必须存在于至少两个日志中才能使证书有效。
此功能的plist键是NSRequiresCertificateTransparency
。 启用此选项将强制执行证书透明性。 在iOS 10和macOS 10.12及更高版本上可用。
证书和公钥固定
当您购买证书以在服务器上使用HTTPS时,该证书被认为是合法的,因为它是使用中间证书颁发机构的证书签名的。 只要最后一个证书是由受信任的根证书颁发机构签名的,则该中间颁发机构使用的证书又可以由另一个中间颁发机构签名,依此类推。
建立HTTPS连接后,会将这些证书提供给客户端。 评估此信任链可确保证书已由iOS已受信任的证书颁发机构正确签名。 (有许多方法可以绕过此检查并接受您自己的自签名证书进行测试,但不要在生产环境中这样做。)
如果信任链中的任何证书无效,则整个证书被认为是无效的,您的数据将不会通过不可信的连接发送出去。 虽然这是一个很好的系统,但并非万无一失。 存在各种弱点,这些弱点可以使iOS信任攻击者的证书而不是合法签名的证书。
例如,拦截代理可能拥有受信任的中间证书。 逆向工程师可以手动指示iOS接受自己的证书。 此外,公司的策略可能已将设备设置为接受其自己的证书。 所有这些使您能够对您的流量执行“中间人”攻击,以使其能够被读取。 但是证书固定将阻止在所有这些情况下建立连接。
通过对照预期证书的副本检查服务器的证书,可以轻松锁定证书。
为了实现固定,必须实现以下委托。 对于URLSession
,使用以下命令:
optional func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
对于NSURLConnection
,您可以使用:
optional func connection(_ connection: NSURLConnection,
didReceive challenge: URLAuthenticationChallenge)
这两种方法都允许您从challenge.protectionSpace.serverTrust
获取SecTrust
对象。 因为我们要覆盖身份验证委托,所以我们现在必须显式调用执行刚刚讨论的标准证书链检查的函数。 通过调用SecTrustEvaluate
函数来执行此SecTrustEvaluate
。 然后,我们可以将服务器的证书与预期的证书进行比较。
这是一个示例实现。
import Foundation
import Security
class URLSessionPinningDelegate: NSObject, URLSessionDelegate
{
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void)
{
var success: Bool = false
if let serverTrust = challenge.protectionSpace.serverTrust
{
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust)
{
//Set policy to validate domain
let policy: SecPolicy = SecPolicyCreateSSL(true, "yourdomain.com" as CFString)
let policies = NSArray.init(object: policy)
SecTrustSetPolicies(serverTrust, policies)
let certificateCount: CFIndex = SecTrustGetCertificateCount(serverTrust)
if certificateCount > 0
{
if let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0)
{
let serverCertificateData = SecCertificateCopyData(certificate) as NSData
//for loop over array which may contain expired + upcoming certificate
let certFilenames: [String] = ["CertificateRenewed", "Certificate"]
for filenameString: String in certFilenames
{
let filePath = Bundle.main.path(forResource: filenameString, ofType: "cer")
if let file = filePath
{
if let localCertData = NSData(contentsOfFile: file)
{
//Set anchor cert to your own server
if let localCert: SecCertificate = SecCertificateCreateWithData(nil, localCertData)
{
let certArray = [localCert] as CFArray
SecTrustSetAnchorCertificates(serverTrust, certArray)
}
//validates a certificate by verifying its signature plus the signatures of the certificates in its certificate chain, up to the anchor certificate
var result = SecTrustResultType.invalid
SecTrustEvaluate(serverTrust, &result);
let isValid: Bool = (result == SecTrustResultType.unspecified || result == SecTrustResultType.proceed)
if (isValid)
{
//Validate host certificate against pinned certificate.
if serverCertificateData.isEqual(to: localCertData as Data)
{
success = true
completionHandler(.useCredential, URLCredential(trust:serverTrust))
break //found a successful certificate, don't need to continue looping
} //end if serverCertificateData.isEqual(to: localCertData as Data)
} //end if (isValid)
} //end if let localCertData = NSData(contentsOfFile: file)
} //end if let file = filePath
} //end for filenameString: String in certFilenames
} //end if let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0)
} //end if certificateCount > 0
} //end if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust)
} //end if let serverTrust = challenge.protectionSpace.serverTrust
if (success == false)
{
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
要使用此代码, URLSession
在创建连接时设置URLSession
的委托。
if let url = NSURL(string: "https://yourdomain.com")
{
let session = URLSession(
configuration: URLSessionConfiguration.ephemeral,
delegate: URLSessionPinningDelegate(),
delegateQueue: nil)
let dataTask = session.dataTask(with: url as URL, completionHandler: { (data, response, error) -> Void in
//...
})
dataTask.resume()
}
确保在您的应用程序捆绑包中包含证书。 如果您的证书是.pem文件,则需要在macOS终端中将其转换为.cer文件:
openssl x509 -inform PEM -in mycert.pem -outform DER -out certificate.cer
现在,如果攻击者更改了证书,则您的应用将检测到该证书并拒绝建立连接。
请注意,某些第三方库(例如AFNetworking)已经支持固定。
消毒与验证
到目前为止,所有的保护措施都可以确保您的连接在中间攻击中非常安全。 即使这样,有关网络通信的一个重要规则也决不能盲目地信任您所接收的数据。 实际上, 按合同进行设计是一种良好的编程习惯。 的 方法的输入和输出具有定义特定接口期望的合同; 如果接口说它将返回NSNumber
,则应该这样做。 如果服务器期望的字符串长度不超过24个字符,请确保该接口最多返回24个字符。
这有助于防止无辜的错误,但更重要的是,它还可以减少各种注入和内存破坏攻击的可能性。 诸如JSONSerialization
类之类的通用解析器会将文本转换为Swift数据类型,可以在其中进行这些类型的测试。
if let dictionary = json as? [String: Any]
{
if let count = dictionary["count"] as? Int
{
//...
其他解析器可以使用与Objective-C等效的对象。 这是一种验证对象在Swift中是否为预期类型的方法。
if someObject is NSArray
在向委托发送方法之前,请确保对象的类型正确,以便它可以响应该方法-否则应用程序将崩溃并显示“无法识别的选择器”错误。
if someObject.responds(to: #selector(getter: NSNumber.intValue)
此外,在尝试向对象发送消息之前,您可以查看其是否符合协议:
if someObject.conforms(to: MyProtocol.self)
或者,您可以检查它是否与Core Foundation对象类型匹配。
if CFGetTypeID(someObject) != CFNullGetTypeID()
仔细选择用户可以从服务器看到的信息是一个好主意。 例如,显示直接从服务器传递消息的错误警报是一个坏主意。 错误消息可能会泄露调试和与安全相关的信息。 一种解决方案是让服务器发送特定的错误代码,这些错误代码使客户端显示预定义的消息。
另外,请确保对URL进行编码,以使其仅包含有效字符。 NSString
的stringByAddingPercentEscapesUsingEncoding
将起作用。 它不对某些字符(例如与号和加号)进行编码,但是CFURLCreateStringByAddingPercentEscapes
函数允许自定义编码内容。
清理用户数据
将数据发送到服务器时,当将任何用户输入传递给将由SQL服务器或将运行代码的服务器执行的命令时,请格外小心。 尽管保护服务器免受此类攻击已超出了本文的范围,但作为移动开发人员,我们可以通过删除服务器所使用语言的字符来做到这一点,以使输入不易受到命令注入攻击。 例如,当特定用户输入不需要引号,分号和斜杠时,可以使用它们。
var mutableString: String = string
mutableString = mutableString.replacingOccurrences(of: "%", with: "")
mutableString = mutableString.replacingOccurrences(of: "\"", with: "")
mutableString = mutableString.replacingOccurrences(of: "\'", with: "")
mutableString = mutableString.replacingOccurrences(of: "\t", with: "")
mutableString = mutableString.replacingOccurrences(of: "\n", with: "")
最好限制用户输入的长度。 我们可以通过设置UITextField
的委托,然后实现其shouldChangeCharactersInRange
委托方法,来限制在文本字段中键入的字符数。
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
{
let newLength: Int = textField.text!.characters.count + string.characters.count - range.length
if newLength > maxSearchLength
{
return false
}
else
{
return true
}
}
对于UITextView,实现此目标的委托方法是:
optional func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool
可以进一步验证用户输入,以便输入具有预期格式。 例如,如果用户要输入电子邮件地址,我们可以检查有效地址:
class func validateEmail(from emailString: String, useStrictValidation isStrict: Bool) -> Bool
{
var filterString: String? = nil
if isStrict
{
filterString = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}"
}
else
{
filterString = ".+@.+\\.[A-Za-z]{2}[A-Za-z]*"
}
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", filterString!)
return emailPredicate.evaluate(with: emailString)
}
如果用户正在将图像上传到服务器,我们可以检查该图像是否有效。 例如,对于JPEG文件,前两个字节和后两个字节始终为FF D8和FF D9。
class func validateImageData(_ data: Data) -> Bool
{
let totalBytes: Int = data.count
if totalBytes < 12
{
return false
}
let bytes = [UInt8](data)
let isValid: Bool = (bytes[0] == UInt8(0xff) && bytes[1] == UInt8(0xd8) && bytes[totalBytes - 2] == UInt8(0xff) && bytes[totalBytes - 1] == UInt8(0xd9))
return isValid
}
清单继续进行,但是只有您(作为开发人员)才能知道给定设计要求的预期输入和输出。
URL缓存
您通过网络发送的数据有可能被缓存在内存和设备存储中。 正如我们一直在做的那样,您可以竭尽全力保护您的网络通信,只是发现通信已被存储。
当涉及到缓存设置时,各种版本的iOS都有一些意外的行为,有关在iOS中缓存的内容的一些规则会不断更改版本。 虽然缓存可以通过减少请求数量来提高网络性能,但对于您认为高度敏感的任何数据都将其关闭可能是个好主意。 您可以随时调用以下方法来删除共享缓存:
URLCache.shared.removeAllCachedResponses()
要在全局级别上禁用缓存,请使用:
let theURLCache = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
URLCache.shared = theURLCache
而且,如果您使用URLSession
,则可以像这样禁用会话的缓存:
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
configuration.urlCache = nil
let session = URLSession.init(configuration: configuration)
如果您将NSURLConnection
对象与委托一起使用,则可以使用此委托方法禁用每个连接的缓存:
func connection(_ connection: NSURLConnection, willCacheResponse cachedResponse: CachedURLResponse) -> CachedURLResponse?
{
return nil
}
并创建一个不检查缓存的URL请求,请使用:
var request = NSMutableURLRequest(url: theUrl, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: urlTimeoutTime)
各种版本的iOS 8 都有一些错误 ,这些错误单独依靠其中的某些方法不会起作用。 这意味着在需要可靠地防止缓存网络请求时,为敏感连接实现上述所有代码是个好主意。
未来
了解HTTPS的限制对于保护网络通信非常重要。
在大多数情况下,HTTPS在服务器处停止。 例如,我与公司服务器的连接可能是通过HTTPS进行的,但是一旦该流量到达服务器,它便不会被加密。 这意味着公司将能够查看发送的信息(在大多数情况下需要),并且这也意味着公司可以随后将该代理进行代理或将其再次未经加密地传递出去。
在结束本文之前,我不能不涉及另一个新概念,即所谓的“端到端加密”。 一个很好的例子是一个加密的聊天应用程序,其中两个移动设备通过服务器相互通信。 这两个设备创建公用密钥和专用密钥,它们交换公用密钥,而它们的专用密钥永远不会离开设备。 数据仍通过服务器通过HTTPS发送,但是首先由另一方的公钥加密,这样只有持有私钥的设备才能解密彼此的消息。
可以帮助您理解端到端加密的类比,想象一下我希望有人安全地向我发送一条消息,只有我可以阅读。 因此,我为他们提供了一个带有打开的挂锁(公用密钥)的盒子,而我保留了挂锁(专用密钥)。 用户编写一条消息,将其放入框中,锁定挂锁,然后将其发送回给我。 只有我能读懂消息的内容,因为我是唯一拥有解锁挂锁钥匙的人。
通过端到端加密,服务器可以提供通信服务,但是它无法读取通信内容,它们附带了锁盒,但是他们没有打开它的钥匙。 尽管实现细节不在本文讨论范围之内,但是如果您希望允许应用程序用户之间的安全通信,则这是一个强大的概念。
如果您想了解更多有关此方法的信息,那么一个开始的地方就是开源项目Open Whisper System的GitHub存储库 。
结论
如今,几乎所有移动应用程序都将通过网络进行通信,而安全性是移动应用程序开发中至关重要但却经常被忽略的方面。
在本文中,我们介绍了一些安全最佳实践,包括简单的HTTPS,网络通信的应用程序强化,数据清理和端到端加密。 这些最佳做法应为编写移动应用程序时的安全性奠定基础。
翻译自: https://code.tutsplus.com/articles/securing-communications-on-ios--cms-28529