Challenge-response, SwiftData, new UI
This commit is contained in:
@ -1,90 +0,0 @@
|
||||
import Foundation
|
||||
import NIOCore
|
||||
|
||||
/**
|
||||
Encapsulates a response from a device.
|
||||
*/
|
||||
struct DeviceResponse {
|
||||
|
||||
/// Shorthand property for a timeout event.
|
||||
static var deviceTimedOut: DeviceResponse {
|
||||
.init(event: .deviceTimedOut)
|
||||
}
|
||||
|
||||
/// Shorthand property for a disconnected event.
|
||||
static var deviceNotConnected: DeviceResponse {
|
||||
.init(event: .deviceNotConnected)
|
||||
}
|
||||
|
||||
/// Shorthand property for a connected event.
|
||||
static var deviceConnected: DeviceResponse {
|
||||
.init(event: .deviceConnected)
|
||||
}
|
||||
|
||||
/// Shorthand property for an unexpected socket event.
|
||||
static var unexpectedSocketEvent: DeviceResponse {
|
||||
.init(event: .unexpectedSocketEvent)
|
||||
}
|
||||
|
||||
/// Shorthand property for an invalid message.
|
||||
static var invalidMessageSize: DeviceResponse {
|
||||
.init(event: .invalidMessageSize)
|
||||
}
|
||||
|
||||
/// Shorthand property for missing body data.
|
||||
static var noBodyData: DeviceResponse {
|
||||
.init(event: .noBodyData)
|
||||
}
|
||||
|
||||
/// Shorthand property for a busy connection
|
||||
static var operationInProgress: DeviceResponse {
|
||||
.init(event: .operationInProgress)
|
||||
}
|
||||
|
||||
/// The response to a key from the server
|
||||
let event: MessageResult
|
||||
|
||||
/// The index of the next key to use
|
||||
let response: Message?
|
||||
|
||||
/**
|
||||
Decode a message from a buffer.
|
||||
|
||||
The buffer must contain `Message.length+1` bytes. The first byte denotes the event type,
|
||||
the remaining bytes contain the message.
|
||||
- Parameter buffer: The buffer where the message bytes are stored
|
||||
*/
|
||||
init?(_ buffer: ByteBuffer) {
|
||||
guard let byte = buffer.getBytes(at: 0, length: 1) else {
|
||||
print("No bytes received from device")
|
||||
return nil
|
||||
}
|
||||
guard let event = MessageResult(rawValue: byte[0]) else {
|
||||
print("Unknown response \(byte[0]) received from device")
|
||||
return nil
|
||||
}
|
||||
self.event = event
|
||||
guard let data = buffer.getSlice(at: 1, length: Message.length) else {
|
||||
self.response = nil
|
||||
return
|
||||
}
|
||||
self.response = Message(decodeFrom: data)
|
||||
}
|
||||
|
||||
/**
|
||||
Create a response from an event without a message from the device.
|
||||
- Parameter event: The response from the device.
|
||||
*/
|
||||
init(event: MessageResult) {
|
||||
self.event = event
|
||||
self.response = nil
|
||||
}
|
||||
|
||||
/// Get the reponse encoded in bytes.
|
||||
var encoded: Data {
|
||||
guard let message = response else {
|
||||
return event.encoded
|
||||
}
|
||||
return event.encoded + message.encoded
|
||||
}
|
||||
}
|
17
Sesame/API/Extensions/Data+Coding.swift
Normal file
17
Sesame/API/Extensions/Data+Coding.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
|
||||
func convert<T>(into value: T) -> T {
|
||||
withUnsafeBytes {
|
||||
$0.baseAddress!.load(as: T.self)
|
||||
}
|
||||
}
|
||||
|
||||
init<T>(from value: T) {
|
||||
var target = value
|
||||
self = Swift.withUnsafeBytes(of: &target) {
|
||||
Data($0)
|
||||
}
|
||||
}
|
||||
}
|
@ -40,20 +40,3 @@ extension Data {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
|
||||
|
||||
func convert<T>(into value: T) -> T {
|
||||
withUnsafeBytes {
|
||||
$0.baseAddress!.load(as: T.self)
|
||||
}
|
||||
}
|
||||
|
||||
init<T>(from value: T) {
|
||||
var target = value
|
||||
self = Swift.withUnsafeBytes(of: &target) {
|
||||
Data($0)
|
||||
}
|
||||
}
|
||||
}
|
@ -15,4 +15,7 @@ extension UInt32 {
|
||||
var encoded: Data {
|
||||
Data(from: CFSwapInt32HostToLittle(self))
|
||||
}
|
||||
|
||||
/// The size of a `UInt32` when converted to data
|
||||
static let byteSize = MemoryLayout<UInt32>.size
|
||||
}
|
8
Sesame/API/Message+Size.swift
Normal file
8
Sesame/API/Message+Size.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
extension Message {
|
||||
|
||||
/// The byte length of an encoded message content
|
||||
static let size: Int = 2 + 2 * UInt32.byteSize
|
||||
|
||||
}
|
@ -1,179 +0,0 @@
|
||||
import Foundation
|
||||
import NIOCore
|
||||
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
/**
|
||||
An authenticated message to or from the device.
|
||||
*/
|
||||
struct Message: Equatable, Hashable {
|
||||
|
||||
/// The message authentication code for the message (32 bytes)
|
||||
let mac: Data
|
||||
|
||||
/// The message content
|
||||
let content: Content
|
||||
|
||||
/**
|
||||
Create an authenticated message
|
||||
- Parameter mac: The message authentication code
|
||||
- Parameter content: The message content
|
||||
*/
|
||||
init(mac: Data, content: Content) {
|
||||
self.mac = mac
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
|
||||
extension Message: Codable {
|
||||
|
||||
enum CodingKeys: Int, CodingKey {
|
||||
case mac = 1
|
||||
case content = 2
|
||||
}
|
||||
}
|
||||
|
||||
extension Message {
|
||||
|
||||
/**
|
||||
The message content without authentication.
|
||||
*/
|
||||
struct Content: Equatable, Hashable {
|
||||
|
||||
/// The time of message creation, in UNIX time (seconds since 1970)
|
||||
let time: UInt32
|
||||
|
||||
/// The counter of the message (for freshness)
|
||||
let id: UInt32
|
||||
|
||||
let deviceId: UInt8?
|
||||
|
||||
/**
|
||||
Create new message content.
|
||||
- Parameter time: The time of message creation,
|
||||
- Parameter id: The counter of the message
|
||||
*/
|
||||
init(time: UInt32, id: UInt32, device: UInt8) {
|
||||
self.time = time
|
||||
self.id = id
|
||||
self.deviceId = device
|
||||
}
|
||||
|
||||
/**
|
||||
Decode message content from data.
|
||||
|
||||
The data consists of two `UInt32` encoded in little endian format
|
||||
- Warning: The sequence must contain at least 8 bytes, or the function will crash.
|
||||
- Parameter data: The sequence containing the bytes.
|
||||
*/
|
||||
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
|
||||
self.time = UInt32(data: Data(data.prefix(MemoryLayout<UInt32>.size)))
|
||||
self.id = UInt32(data: Data(data.dropLast().suffix(MemoryLayout<UInt32>.size)))
|
||||
self.deviceId = data.suffix(1).last!
|
||||
}
|
||||
|
||||
/// The byte length of an encoded message content
|
||||
static var length: Int {
|
||||
MemoryLayout<UInt32>.size * 2 + 1
|
||||
}
|
||||
|
||||
/// The message content encoded to data
|
||||
var encoded: Data {
|
||||
time.encoded + id.encoded + Data([deviceId ?? 0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Message.Content: Codable {
|
||||
|
||||
enum CodingKeys: Int, CodingKey {
|
||||
case time = 1
|
||||
case id = 2
|
||||
case deviceId = 3
|
||||
}
|
||||
}
|
||||
|
||||
extension Message {
|
||||
|
||||
/// The length of a message in bytes
|
||||
static var length: Int {
|
||||
SHA256.byteCount + Content.length
|
||||
}
|
||||
|
||||
/**
|
||||
Decode a message from a byte buffer.
|
||||
The buffer must contain at least `Message.length` bytes, or it will return `nil`.
|
||||
- Parameter buffer: The buffer containing the bytes.
|
||||
*/
|
||||
init?(decodeFrom buffer: ByteBuffer) {
|
||||
guard let data = buffer.getBytes(at: 0, length: Message.length) else {
|
||||
return nil
|
||||
}
|
||||
self.init(decodeFrom: data)
|
||||
}
|
||||
|
||||
init?(decodeFrom data: Data, index: inout Int) {
|
||||
guard index + Message.length <= data.count else {
|
||||
return nil
|
||||
}
|
||||
self.init(decodeFrom: data.advanced(by: index))
|
||||
index += Message.length
|
||||
}
|
||||
|
||||
/// The message encoded to data
|
||||
var encoded: Data {
|
||||
mac + content.encoded
|
||||
}
|
||||
|
||||
var bytes: [UInt8] {
|
||||
Array(encoded)
|
||||
}
|
||||
|
||||
/**
|
||||
Create a message from received bytes.
|
||||
- Parameter data: The sequence of bytes
|
||||
- Note: The sequence must contain at least `Message.length` bytes, or the function will crash.
|
||||
*/
|
||||
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
|
||||
let count = SHA256.byteCount
|
||||
self.mac = Data(data.prefix(count))
|
||||
self.content = .init(decodeFrom: Array(data.dropFirst(count)))
|
||||
}
|
||||
|
||||
/**
|
||||
Check if the message contains a valid authentication code
|
||||
- Parameter key: The key used to sign the message.
|
||||
- Returns: `true`, if the message is valid.
|
||||
*/
|
||||
func isValid(using key: SymmetricKey) -> Bool {
|
||||
HMAC<SHA256>.isValidAuthenticationCode(mac, authenticating: content.encoded, using: key)
|
||||
}
|
||||
}
|
||||
|
||||
extension Message.Content {
|
||||
|
||||
/**
|
||||
Calculate an authentication code for the message content.
|
||||
- Parameter key: The key to use to sign the content.
|
||||
- Returns: The new message signed with the key.
|
||||
*/
|
||||
func authenticate(using key: SymmetricKey) -> Message {
|
||||
let mac = HMAC<SHA256>.authenticationCode(for: encoded, using: key)
|
||||
return .init(mac: Data(mac.map { $0 }), content: self)
|
||||
}
|
||||
|
||||
/**
|
||||
Calculate an authentication code for the message content and convert everything to data.
|
||||
- Parameter key: The key to use to sign the content.
|
||||
- Returns: The new message signed with the key, serialized to bytes.
|
||||
*/
|
||||
func authenticateAndSerialize(using key: SymmetricKey) -> Data {
|
||||
let encoded = self.encoded
|
||||
let mac = HMAC<SHA256>.authenticationCode(for: encoded, using: key)
|
||||
return Data(mac.map { $0 }) + encoded
|
||||
}
|
||||
}
|
@ -4,93 +4,232 @@ import Foundation
|
||||
A result from sending a key to the device.
|
||||
*/
|
||||
enum MessageResult: UInt8 {
|
||||
|
||||
// MARK: Device status
|
||||
|
||||
/// Text content was received, although binary data was expected
|
||||
/// The message was accepted.
|
||||
case messageAccepted = 0
|
||||
|
||||
/// The web socket received text while waiting for binary data.
|
||||
case textReceived = 1
|
||||
|
||||
/// A socket event on the device was unexpected (not binary data)
|
||||
/// An unexpected socket event occured while performing the exchange.
|
||||
case unexpectedSocketEvent = 2
|
||||
|
||||
/// The size of the payload (i.e. message) was invalid
|
||||
case invalidMessageSize = 3
|
||||
/// The received message size is invalid.
|
||||
case invalidMessageSizeFromRemote = 3
|
||||
|
||||
/// The transmitted message could not be authenticated using the key
|
||||
case messageAuthenticationFailed = 4
|
||||
/// The message signature was incorrect.
|
||||
case invalidSignatureFromRemote = 4
|
||||
|
||||
/// The message time was not within the acceptable bounds
|
||||
case messageTimeMismatch = 5
|
||||
/// The server challenge of the message did not match previous messages
|
||||
case invalidServerChallengeFromRemote = 5
|
||||
|
||||
/// A later key has been used, invalidating this key (to prevent replay attacks after blocked communication)
|
||||
case messageCounterInvalid = 6
|
||||
/// The client challenge of the message did not match previous messages
|
||||
case invalidClientChallengeFromRemote = 6
|
||||
|
||||
/// The key was accepted by the device, and the door will be opened
|
||||
case messageAccepted = 7
|
||||
/// An unexpected or unsupported message type was received
|
||||
case invalidMessageTypeFromRemote = 7
|
||||
|
||||
/// A message is already being processed
|
||||
case tooManyRequests = 8
|
||||
|
||||
/// The device id is invalid
|
||||
case messageDeviceInvalid = 8
|
||||
/// The received message result was not ``messageAccepted``
|
||||
case invalidMessageResultFromRemote = 9
|
||||
|
||||
/// An invalid Url parameter was set sending a message to the device over a local connection
|
||||
case invalidUrlParameter = 10
|
||||
|
||||
// MARK: Server status
|
||||
|
||||
/// The body data posting a message was missing or of wrong length
|
||||
case noOrInvalidBodyDataFromRemote = 21
|
||||
|
||||
/// The authentication token for the server was invalid
|
||||
case invalidServerAuthenticationFromRemote = 22
|
||||
|
||||
/// The request took too long to complete
|
||||
case deviceTimedOut = 23
|
||||
|
||||
/// The device is not connected to the server via web socket
|
||||
case deviceNotConnected = 24
|
||||
|
||||
/// The device sent a response of invalid size
|
||||
case invalidMessageSizeFromDevice = 25
|
||||
|
||||
/// The header with the authentication token was missing or invalid (not a hex string) from a server request.
|
||||
case missingOrInvalidAuthenticationHeaderFromRemote = 26
|
||||
|
||||
/// The server produced an internal error (500)
|
||||
case internalServerError = 27
|
||||
|
||||
// MARK: Remote status
|
||||
|
||||
/// The request did not contain body data with the key
|
||||
case noBodyData = 10
|
||||
/// The initial state without information about the connection
|
||||
case notChecked = 30
|
||||
|
||||
/// The url string is not a valid url
|
||||
case serverUrlInvalid = 31
|
||||
|
||||
/// The device key or auth token is missing for a request.
|
||||
case noKeyAvailable = 32
|
||||
|
||||
/// The Sesame server behind the proxy could not be found (502)
|
||||
case serviceBehindProxyUnavailable = 33
|
||||
|
||||
/// The server url could not be found (404)
|
||||
case pathOnServerNotFound = 34
|
||||
|
||||
/// The url session request returned an unknown response
|
||||
case unexpectedUrlResponseType = 35
|
||||
|
||||
/// The request to the server returned an unhandled HTTP code
|
||||
case unexpectedServerResponseCode = 36
|
||||
|
||||
/// A valid server challenge was received
|
||||
case deviceAvailable = 37
|
||||
|
||||
case invalidSignatureFromDevice = 38
|
||||
|
||||
case invalidMessageTypeFromDevice = 39
|
||||
|
||||
case unknownMessageResultFromDevice = 40
|
||||
|
||||
/// The device sent a message with an invalid client challenge
|
||||
case invalidClientChallengeFromDevice = 41
|
||||
|
||||
/// The device used an invalid server challenge in a response
|
||||
case invalidServerChallengeFromDevice = 42
|
||||
|
||||
/// The unlock process was successfully completed
|
||||
case unlocked = 43
|
||||
}
|
||||
|
||||
/// The device is not connected
|
||||
case deviceNotConnected = 12
|
||||
|
||||
/// The device did not respond within the timeout
|
||||
case deviceTimedOut = 13
|
||||
|
||||
/// Another message is being processed by the device
|
||||
case operationInProgress = 14
|
||||
|
||||
/// The device is connected
|
||||
case deviceConnected = 15
|
||||
|
||||
case invalidUrlParameter = 20
|
||||
|
||||
case invalidResponseAuthentication = 21
|
||||
extension MessageResult: Error {
|
||||
|
||||
}
|
||||
|
||||
extension MessageResult: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .messageAccepted:
|
||||
return "Message accepted"
|
||||
case .textReceived:
|
||||
return "The device received unexpected text"
|
||||
case .unexpectedSocketEvent:
|
||||
return "Unexpected socket event for the device"
|
||||
case .invalidMessageSize:
|
||||
return "Invalid message data"
|
||||
case .messageAuthenticationFailed:
|
||||
case .invalidMessageSizeFromRemote:
|
||||
return "Invalid message data from remote"
|
||||
case .invalidSignatureFromRemote:
|
||||
return "Message authentication failed"
|
||||
case .messageTimeMismatch:
|
||||
return "Message time invalid"
|
||||
case .messageCounterInvalid:
|
||||
return "Message counter invalid"
|
||||
case .messageAccepted:
|
||||
return "Message accepted"
|
||||
case .messageDeviceInvalid:
|
||||
return "Invalid device ID"
|
||||
case .noBodyData:
|
||||
return "No body data included in the request"
|
||||
case .deviceNotConnected:
|
||||
return "Device not connected"
|
||||
case .deviceTimedOut:
|
||||
return "The device did not respond"
|
||||
case .operationInProgress:
|
||||
return "Another operation is in progress"
|
||||
case .deviceConnected:
|
||||
return "The device is connected"
|
||||
case .invalidServerChallengeFromRemote:
|
||||
return "Remote used wrong server challenge"
|
||||
case .invalidClientChallengeFromRemote:
|
||||
return "Wrong client challenge sent"
|
||||
case .invalidMessageTypeFromRemote:
|
||||
return "Message type from remote invalid"
|
||||
case .tooManyRequests:
|
||||
return "Device busy"
|
||||
case .invalidMessageResultFromRemote:
|
||||
return "Invalid message result"
|
||||
case .invalidUrlParameter:
|
||||
return "The url parameter could not be found"
|
||||
case .invalidResponseAuthentication:
|
||||
return "The response could not be authenticated"
|
||||
|
||||
case .noOrInvalidBodyDataFromRemote:
|
||||
return "Invalid body data in server request"
|
||||
case .invalidServerAuthenticationFromRemote:
|
||||
return "Invalid server token"
|
||||
case .deviceTimedOut:
|
||||
return "The device did not respond"
|
||||
case .deviceNotConnected:
|
||||
return "Device not connected to server"
|
||||
case .invalidMessageSizeFromDevice:
|
||||
return "Invalid device message size"
|
||||
case .missingOrInvalidAuthenticationHeaderFromRemote:
|
||||
return "Invalid server token format"
|
||||
case .internalServerError:
|
||||
return "Internal server error"
|
||||
|
||||
case .notChecked:
|
||||
return "Not checked"
|
||||
case .serverUrlInvalid:
|
||||
return "Invalid server url"
|
||||
case .noKeyAvailable:
|
||||
return "No key available"
|
||||
case .serviceBehindProxyUnavailable:
|
||||
return "Service behind proxy not found"
|
||||
case .pathOnServerNotFound:
|
||||
return "Invalid server path"
|
||||
case .unexpectedUrlResponseType:
|
||||
return "Unexpected URL response"
|
||||
case .unexpectedServerResponseCode:
|
||||
return "Unexpected server response code"
|
||||
case .deviceAvailable:
|
||||
return "Device available"
|
||||
case .invalidSignatureFromDevice:
|
||||
return "Invalid device signature"
|
||||
case .invalidMessageTypeFromDevice:
|
||||
return "Message type from device invalid"
|
||||
case .unknownMessageResultFromDevice:
|
||||
return "Unknown message result"
|
||||
case .invalidClientChallengeFromDevice:
|
||||
return "Device used wrong client challenge"
|
||||
case .invalidServerChallengeFromDevice:
|
||||
return "Invalid"
|
||||
case .unlocked:
|
||||
return "Unlocked"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MessageResult: Codable {
|
||||
|
||||
}
|
||||
|
||||
extension MessageResult {
|
||||
|
||||
var encoded: Data {
|
||||
Data([rawValue])
|
||||
}
|
||||
}
|
||||
|
||||
extension MessageResult {
|
||||
|
||||
init(httpCode: Int) {
|
||||
switch httpCode {
|
||||
case 200: self = .messageAccepted
|
||||
case 204: self = .noOrInvalidBodyDataFromRemote
|
||||
case 403: self = .invalidServerAuthenticationFromRemote
|
||||
case 404: self = .pathOnServerNotFound
|
||||
case 408: self = .deviceTimedOut
|
||||
case 412: self = .deviceNotConnected
|
||||
case 413: self = .invalidMessageSizeFromDevice
|
||||
case 422: self = .missingOrInvalidAuthenticationHeaderFromRemote
|
||||
case 429: self = .tooManyRequests
|
||||
case 500: self = .internalServerError
|
||||
case 501: self = .unexpectedServerResponseCode
|
||||
case 502: self = .serviceBehindProxyUnavailable
|
||||
default: self = .unexpectedServerResponseCode
|
||||
}
|
||||
}
|
||||
|
||||
var statusCode: Int {
|
||||
switch self {
|
||||
case .messageAccepted: return 200 // ok
|
||||
case .noOrInvalidBodyDataFromRemote: return 204 // noContent
|
||||
case .invalidServerAuthenticationFromRemote: return 403 // forbidden
|
||||
case .pathOnServerNotFound: return 404 // notFound
|
||||
case .deviceTimedOut: return 408 // requestTimeout
|
||||
case .invalidMessageSizeFromRemote: return 411 // lengthRequired
|
||||
case .deviceNotConnected: return 412 // preconditionFailed
|
||||
case .invalidMessageSizeFromDevice: return 413 // payloadTooLarge
|
||||
case .missingOrInvalidAuthenticationHeaderFromRemote: return 422 // unprocessableEntity
|
||||
case .tooManyRequests: return 429 // tooManyRequests
|
||||
case .internalServerError: return 500 // internalServerError
|
||||
case .unexpectedServerResponseCode: return 501 // notImplemented
|
||||
case .serviceBehindProxyUnavailable: return 502 // badGateway
|
||||
default: return 501 // == unexpectedServerResponseCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
import Foundation
|
||||
import NIOCore
|
||||
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
struct ServerMessage {
|
||||
|
||||
static let authTokenSize = SHA256.byteCount
|
||||
|
||||
let authToken: Data
|
||||
|
||||
let message: Message
|
||||
|
||||
init(authToken: Data, message: Message) {
|
||||
self.authToken = authToken
|
||||
self.message = message
|
||||
}
|
||||
|
||||
var encoded: Data {
|
||||
authToken + message.encoded
|
||||
}
|
||||
}
|
14
Sesame/API/SesameHeader.swift
Normal file
14
Sesame/API/SesameHeader.swift
Normal file
@ -0,0 +1,14 @@
|
||||
import Foundation
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
enum SesameHeader {
|
||||
|
||||
static let authenticationHeader = "Authorization"
|
||||
|
||||
static let serverAuthenticationTokenSize = SHA256.byteCount
|
||||
|
||||
}
|
@ -3,10 +3,7 @@ import Foundation
|
||||
/**
|
||||
The active urls on the server, for the device and the remote to connect
|
||||
*/
|
||||
enum RouteAPI: String {
|
||||
|
||||
/// Check the device status
|
||||
case getDeviceStatus = "status"
|
||||
enum SesameRoute: String {
|
||||
|
||||
/// Send a message to the server, to relay to the device
|
||||
case postMessage = "message"
|
15
Sesame/API/SignedMessage+Size.swift
Normal file
15
Sesame/API/SignedMessage+Size.swift
Normal file
@ -0,0 +1,15 @@
|
||||
import Foundation
|
||||
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
extension SignedMessage {
|
||||
|
||||
/// The length of a message in bytes
|
||||
static var size: Int {
|
||||
SHA256.byteCount + Message.size
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user