Skip to content

Commit

Permalink
Merge branch 'feature/sec2_provisioning' into 'master'
Browse files Browse the repository at this point in the history
Support for provisioning using Security 2.

See merge request idf/esp-idf-provisioning-ios!38
  • Loading branch information
shahpiyushv committed Sep 30, 2022
2 parents 197e326 + 869a2bc commit 4668cda
Show file tree
Hide file tree
Showing 47 changed files with 4,353 additions and 305 deletions.
4 changes: 2 additions & 2 deletions ESPProvision.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |spec|

spec.name = "ESPProvision"
spec.version = "2.0.16"
spec.version = "2.1.0"
spec.summary = "ESP-IDF provisioning in Swift"
spec.description = "It provides mechanism to provide network credentials and/or custom data to an ESP32, ESP32-S2 or ESP8266 devices"
spec.homepage = "https://github.com/espressif/esp-idf-provisioning-ios"
Expand All @@ -22,7 +22,7 @@ Pod::Spec.new do |spec|
}

spec.author = "Espressif Systems"
spec.platform = :ios, "11.0"
spec.platform = :ios, "13.0"
spec.source = { :git => "https://github.com/espressif/esp-idf-provisioning-ios.git", :tag => "lib-#{spec.version}" }

spec.source_files = "ESPProvision", "ESPProvision/**/*.{h,m,swift}"
Expand Down
134 changes: 126 additions & 8 deletions ESPProvision.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

79 changes: 64 additions & 15 deletions ESPProvision/ESPDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,22 @@ public protocol ESPBLEDelegate {
/// Class needs to conform to `ESPDeviceConnectionDelegate` protocol when trying to establish a connection.
public protocol ESPDeviceConnectionDelegate {
/// Get Proof of possession for an `ESPDevice` from object conforming `ESPDeviceConnectionDelegate` protocol.
/// POP is needed when security scheme is sec1 or sec2.
/// For other security scheme return nil in completionHandler.
///
/// - Parameters:
/// - forDevice: `ESPDevice`for which Proof of possession is needed.
/// - completionHandler: Call this method to return POP needed for initialting session with the device.
func getProofOfPossesion(forDevice: ESPDevice, completionHandler: @escaping (String) -> Void)

/// Get username for an `ESPDevice` from object conforming `ESPDeviceConnectionDelegate` protocol.
/// Client needs to handle this delegate in case security scheme is sec2.
/// For other schemes return nil for username.
///
/// - Parameters:
/// - forDevice: `ESPDevice`for which username is needed.
/// - completionHandler: Call this method to return username needed for initialting session with the device.
func getUsername(forDevice: ESPDevice, completionHandler: @escaping (_ username: String?) -> Void)
}

/// The `ESPDevice` class is the main inteface for managing a device. It encapsulates method and properties
Expand Down Expand Up @@ -110,6 +121,8 @@ open class ESPDevice {
public var versionInfo:NSDictionary?
/// Store BLE delegate information
public var bleDelegate: ESPBLEDelegate?
/// Store username for sec1
public var username: String?
/// Advertisement data for BLE device
/// This property is read-only
public private(set) var advertisementData:[String:Any]?
Expand All @@ -132,11 +145,12 @@ open class ESPDevice {
/// - transport: Mode of transport.
/// - proofOfPossession: Pop of device.
/// - softAPPassword: Password in case SoftAP device.
public init(name: String, security: ESPSecurity, transport: ESPTransport, proofOfPossession:String? = nil, softAPPassword:String? = nil, advertisementData: [String:Any]? = nil) {
public init(name: String, security: ESPSecurity, transport: ESPTransport, proofOfPossession:String? = nil, username:String? = nil, softAPPassword:String? = nil, advertisementData: [String:Any]? = nil) {
ESPLog.log("Intializing ESPDevice with name:\(name), security:\(security), transport:\(transport), proofOfPossession:\(proofOfPossession ?? "nil") and softAPPassword:\(softAPPassword ?? "nil")")
self.deviceName = name
self.security = security
self.transport = transport
self.username = username
self.proofOfPossession = proofOfPossession
self.softAPPassword = softAPPassword
self.advertisementData = advertisementData
Expand Down Expand Up @@ -430,31 +444,46 @@ open class ESPDevice {
open func initialiseSession(sessionPath: String?, completionHandler: @escaping (ESPSessionStatus) -> Void) {
ESPLog.log("Initialise session")

if let capability = self.capabilities, capability.contains(ESPConstants.noSecCapability) {
if security != .unsecure {
completionHandler(.failedToConnect(.securityMismatch))
return
}
// Determine security scheme of current device using capabilities
var securityScheme: ESPSecurity = .secure2
if let prov = versionInfo?[ESPConstants.provKey] as? NSDictionary, let secScheme = prov[ESPConstants.securityScheme] as? Int {
securityScheme = ESPSecurity.init(rawValue: secScheme)
} else if let capability = self.capabilities, capability.contains(ESPConstants.noSecCapability) {
securityScheme = .unsecure
} else {
if security != .secure {
completionHandler(.failedToConnect(.securityMismatch))
return
}
securityScheme = .secure
}

// Unsecure communication should only be allowed if explicitily configured in both library and device
if (security == .unsecure || securityScheme == .unsecure) && security != securityScheme {
completionHandler(.failedToConnect(.securityMismatch))
return
}

switch security {
switch securityScheme {
case .secure2:
// POP is mandatory for secure 2
guard let pop = proofOfPossession else {
delegate?.getProofOfPossesion(forDevice: self, completionHandler: { popString in
self.getUsernameForSecure2(sessionPath: sessionPath, password: popString, completionHandler: completionHandler)
})
return
}
getUsernameForSecure2(sessionPath: sessionPath, password: pop, completionHandler: completionHandler)
case .secure:
var pop:String!
if let capability = self.capabilities, capability.contains(ESPConstants.noProofCapability) {
initSecureSession(sessionPath: sessionPath, pop: "", completionHandler: completionHandler)
} else {
if self.proofOfPossession == nil {
delegate?.getProofOfPossesion(forDevice: self, completionHandler: { popString in
self.initSecureSession(sessionPath: sessionPath, pop: popString, completionHandler: completionHandler)
self.initSecureSession(sessionPath: sessionPath, pop: popString , completionHandler: completionHandler)
})
} else {
pop = self.proofOfPossession ?? ""
self.initSecureSession(sessionPath: sessionPath, pop: pop, completionHandler: completionHandler)
if let pop = proofOfPossession {
self.initSecureSession(sessionPath: sessionPath, pop: pop, completionHandler: completionHandler)
} else {
completionHandler(.failedToConnect(.noPOP))
}
}
}
case .unsecure:
Expand All @@ -470,6 +499,26 @@ open class ESPDevice {
initSession(sessionPath: sessionPath, completionHandler: completionHandler)
}

func getUsernameForSecure2(sessionPath: String?, password: String, completionHandler: @escaping (ESPSessionStatus) -> Void) {
if let username = username {
initSecure2Session(sessionPath: sessionPath, username: username, password: password, completionHandler: completionHandler)
} else {
delegate?.getUsername(forDevice: self, completionHandler: { usernameString in
if usernameString == nil {
completionHandler(.failedToConnect(.noUsername))
} else {
self.initSecure2Session(sessionPath: sessionPath, username: usernameString!, password: password, completionHandler: completionHandler)
}
})
}
}

func initSecure2Session(sessionPath: String?, username: String, password: String, completionHandler: @escaping (ESPSessionStatus) -> Void) {
ESPLog.log("Initialise session security 2")
securityLayer = ESPSecurity2(username: username, password: password)
initSession(sessionPath: sessionPath, completionHandler: completionHandler)
}

func initSession(sessionPath: String?, completionHandler: @escaping (ESPSessionStatus) -> Void) {
ESPLog.log("Init session")
switch transport {
Expand Down
73 changes: 49 additions & 24 deletions ESPProvision/ESPProvisionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,42 @@ import CoreBluetooth
import AVFoundation

/// Supported mode of communication with device.
public enum ESPTransport {
public enum ESPTransport: String {
/// Communicate using bluetooth.
case ble
/// Communicate using Soft Access Point.
case softap

public init?(rawValue: String) {
switch rawValue.lowercased() {
case "ble": self = .ble
case "softap": self = .softap
default: return nil
}
}
}

/// Security options on data transmission.
public enum ESPSecurity: Int {
/// Unsecure data transmission.
case unsecure = 0
/// Data is encrypted before transmission.
case secure = 1
case secure = 1
/// Data is encrypted using SRP algorithm before transmission
case secure2 = 2

public init(rawValue: Int) {
switch rawValue {
case 0:
self = .unsecure
case 1:
self = .secure
case 2:
self = .secure2
default:
self = .secure2
}
}
}

/// The `ESPProvisionManager` class is a singleton class. It provides methods for getting `ESPDevice` object.
Expand All @@ -45,7 +68,7 @@ public class ESPProvisionManager: NSObject, AVCaptureMetadataOutputObjectsDelega
private var espBleTransport:ESPBleTransport!
private var devicePrefix = ""
private var transport:ESPTransport = .ble
private var security: ESPSecurity = .secure
private var security: ESPSecurity = .secure2
private var searchCompletionHandler: (([ESPDevice]?,ESPDeviceCSSError?) -> Void)?
private var scanCompletionHandler: ((ESPDevice?,ESPDeviceCSSError?) -> Void)?
private var captureSession: AVCaptureSession!
Expand Down Expand Up @@ -261,27 +284,28 @@ public class ESPProvisionManager: NSObject, AVCaptureMetadataOutputObjectsDelega
ESPLog.log("Parsing QR code response...code:\(code)")
self.scanStatusBlock?(.readingCode)

if let jsonArray = try? JSONSerialization.jsonObject(with: Data(code.utf8), options: []) as? [String: String] {
if let deviceName = jsonArray["name"], let transportInfo = jsonArray["transport"] {
if (transportInfo.lowercased() == "softap" || transportInfo.lowercased() == "ble"){
let transport:ESPTransport = transportInfo.lowercased() == "softap" ? .softap:.ble
let security:ESPSecurity = jsonArray["security"] ?? "1" == "0" ? .unsecure:.secure
let pop = jsonArray["pop"] ?? ""
if let scanCompletionHandler = scanCompletionHandler {
switch transport {
case .ble:
createESPDevice(deviceName: deviceName, transport: transport, security: security, proofOfPossession: pop, completionHandler: scanCompletionHandler)
default:
createESPDevice(deviceName: deviceName, transport: transport, security: security, proofOfPossession: pop, softAPPassword: jsonArray["password"] ?? "", completionHandler: scanCompletionHandler)

}
}
return
guard let scanCompletionHandler = scanCompletionHandler else {
return
}

if let jsonData = code.data(using: .utf8) {
do {
let decoder = JSONDecoder()
let decodeResponse = try decoder.decode(ESPScanResult.self, from: jsonData)
switch decodeResponse.transport {
case .ble:
createESPDevice(deviceName: decodeResponse.name, transport: decodeResponse.transport, security: decodeResponse.security, proofOfPossession: decodeResponse.pop, username: decodeResponse.username, completionHandler: scanCompletionHandler)
default:
createESPDevice(deviceName: decodeResponse.name, transport: decodeResponse.transport, security: decodeResponse.security, proofOfPossession: decodeResponse.pop, softAPPassword: decodeResponse.password ?? "", username: decodeResponse.username, completionHandler: scanCompletionHandler)

}
} catch {
scanCompletionHandler(nil,.invalidQRCode(code))
}
} else {
ESPLog.log("Invalid QR code.")
scanCompletionHandler(nil,.invalidQRCode(code))
}
ESPLog.log("Invalid QR code.")
scanCompletionHandler?(nil,.invalidQRCode(code))
}

/// Manually create `ESPDevice` object.
Expand All @@ -292,7 +316,7 @@ public class ESPProvisionManager: NSObject, AVCaptureMetadataOutputObjectsDelega
/// - security: Security mode for communication.
/// - completionHandler: The completion handler is invoked with parameters containing newly created device object.
/// Error in case where method fails to return a device object.
public func createESPDevice(deviceName: String, transport: ESPTransport, security: ESPSecurity = .secure, proofOfPossession:String? = nil, softAPPassword:String? = nil, completionHandler: @escaping (ESPDevice?,ESPDeviceCSSError?) -> Void) {
public func createESPDevice(deviceName: String, transport: ESPTransport, security: ESPSecurity = .secure2, proofOfPossession:String? = nil, softAPPassword:String? = nil, username:String? = nil, completionHandler: @escaping (ESPDevice?,ESPDeviceCSSError?) -> Void) {

ESPLog.log("Creating ESPDevice...")

Expand All @@ -302,11 +326,11 @@ public class ESPProvisionManager: NSObject, AVCaptureMetadataOutputObjectsDelega
self.scanCompletionHandler = completionHandler
self.security = security
self.scanStatusBlock?(.searchingBLE(deviceName))
espBleTransport = ESPBleTransport(scanTimeout: 5.0, deviceNamePrefix: deviceName, proofOfPossession: proofOfPossession)
espBleTransport = ESPBleTransport(scanTimeout: 5.0, deviceNamePrefix: deviceName, proofOfPossession: proofOfPossession, username: username)
espBleTransport.scan(delegate: self)
default:
self.scanStatusBlock?(.joiningSoftAP(deviceName))
let newDevice = ESPDevice(name: deviceName, security: security, transport: transport,proofOfPossession: proofOfPossession, softAPPassword: softAPPassword)
let newDevice = ESPDevice(name: deviceName, security: security, transport: transport, proofOfPossession: proofOfPossession, username:username, softAPPassword: softAPPassword)
ESPLog.log("SoftAp device created successfully.")
completionHandler(newDevice, nil)
}
Expand All @@ -331,6 +355,7 @@ extension ESPProvisionManager: ESPBLETransportDelegate {
for device in peripherals.values {
device.security = self.security
device.proofOfPossession = espBleTransport.proofOfPossession
device.username = espBleTransport.username
device.espBleTransport = espBleTransport
espDevices.append(device)
}
Expand Down
58 changes: 58 additions & 0 deletions ESPProvision/Model/ESPScanResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2022 Espressif Systems
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ESPScanResult.swift
// ESPProvision
//

import Foundation

enum ESPScanError: Error {
case invalidQRCode
}

struct ESPScanResult: Decodable {
var name: String
var username: String?
var pop: String?
var security: ESPSecurity
var transport: ESPTransport
var password: String?

enum CodingKeys: String, CodingKey {
case name
case username
case pop
case security
case transport
case password
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
pop = try container.decodeIfPresent(String.self, forKey: .pop)
let tp = try container.decode(String.self, forKey: .transport)
security = try ESPSecurity.init(rawValue: container.decodeIfPresent(Int.self, forKey: .security) ?? 2)
if security == .secure2 {
username = try container.decodeIfPresent(String.self, forKey: .username)
}
if let transportValue = try ESPTransport.init(rawValue: container.decode(String.self, forKey: .transport)) {
transport = transportValue
} else {
throw ESPScanError.invalidQRCode
}
password = try container.decodeIfPresent(String.self, forKey: .password)
}
}
Loading

0 comments on commit 4668cda

Please sign in to comment.