Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Core] Feature/#273 envelope types #279

Merged
merged 25 commits into from
Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1340"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "WalletConnectKMSTests"
BuildableName = "WalletConnectKMSTests"
BlueprintName = "WalletConnectKMSTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
39 changes: 26 additions & 13 deletions Sources/WalletConnectKMS/Codec/ChaChaPolyCodec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,51 @@ import Foundation
import CryptoKit

protocol Codec {
func encode(plaintext: String, symmetricKey: Data, nonce: ChaChaPoly.Nonce) throws -> String
func decode(sealboxString: String, symmetricKey: Data) throws -> Data
func encode(plaintext: String, symmetricKey: Data, nonce: ChaChaPoly.Nonce) throws -> Data
func decode(sealbox: Data, symmetricKey: Data) throws -> Data
}
extension Codec {
func encode(plaintext: String, symmetricKey: Data, nonce: ChaChaPoly.Nonce = ChaChaPoly.Nonce()) throws -> String {
func encode(plaintext: String, symmetricKey: Data, nonce: ChaChaPoly.Nonce = ChaChaPoly.Nonce()) throws -> Data {
try encode(plaintext: plaintext, symmetricKey: symmetricKey, nonce: nonce)
}
}

class ChaChaPolyCodec: Codec {
/// nonce should always be random, exposed in parameter for testing purpose only
func encode(plaintext: String, symmetricKey: Data, nonce: ChaChaPoly.Nonce) throws -> String {
enum Errors: Error {
case stringToDataFailed(String)
}
/// Secures the given plaintext message with encryption and an authentication tag.
/// - Parameters:
/// - plaintext: plaintext to to encrypt
/// - symmetricKey: symmetric key for encryption
/// - nonce: nonce should always be random, exposed in parameter for testing purpose only
/// - Returns: A combined element composed of the tag, the nonce, and the ciphertext.
/// The data layout of the combined representation is: nonce, ciphertext, then tag.
func encode(plaintext: String, symmetricKey: Data, nonce: ChaChaPoly.Nonce) throws -> Data {
let key = CryptoKit.SymmetricKey(data: symmetricKey)
let dataToSeal = try data(string: plaintext)
let sealBox = try ChaChaPoly.seal(dataToSeal, using: key, nonce: nonce)
return sealBox.combined.base64EncodedString()
return sealBox.combined
}

func decode(sealboxString: String, symmetricKey: Data) throws -> Data {
guard let sealboxData = Data(base64Encoded: sealboxString) else {
throw CodecError.malformedSealbox
}


/// Decrypts the message and verifies its authenticity.
/// - Parameters:
/// - sealbox: The sealed box to open.
/// - symmetricKey: The cryptographic key that was used to seal the message.
/// - Returns: The original plaintext message that was sealed in the box, as long as the correct key is used and authentication succeeds. The call throws an error if decryption or authentication fail.
func decode(sealbox: Data, symmetricKey: Data) throws -> Data {
let sealboxCombined = sealbox
let key = CryptoKit.SymmetricKey(data: symmetricKey)
let sealBox = try ChaChaPoly.SealedBox(combined: sealboxData)
let sealBox = try ChaChaPoly.SealedBox(combined: sealboxCombined)
return try ChaChaPoly.open(sealBox, using: key)
}

private func data(string: String) throws -> Data {
if let data = string.data(using: .utf8) {
return data
} else {
throw CodecError.stringToDataFailed(string)
throw Errors.stringToDataFailed(string)
}
}
}
8 changes: 0 additions & 8 deletions Sources/WalletConnectKMS/Codec/CodecError.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ extension Curve25519.KeyAgreement.PrivateKey: Equatable {

// MARK: - Public Key

public struct AgreementPublicKey: Equatable {
public struct AgreementPublicKey: GenericPasswordConvertible, Equatable {

fileprivate let key: Curve25519.KeyAgreement.PublicKey

Expand Down
30 changes: 22 additions & 8 deletions Sources/WalletConnectKMS/Crypto/KeyManagementService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ public protocol KeyManagementServiceProtocol {
func createX25519KeyPair() throws -> AgreementPublicKey
func createSymmetricKey(_ topic: String) throws -> SymmetricKey
func setPrivateKey(_ privateKey: AgreementPrivateKey) throws
func setPublicKey(publicKey: AgreementPublicKey, for topic: String) throws
func setAgreementSecret(_ agreementSecret: AgreementKeys, topic: String) throws
func setSymmetricKey(_ symmetricKey: SymmetricKey, for topic: String) throws
func getPrivateKey(for publicKey: AgreementPublicKey) throws -> AgreementPrivateKey?
func getAgreementSecret(for topic: String) throws -> AgreementKeys?
func getSymmetricKey(for topic: String) throws -> SymmetricKey?
func getAgreementSecret(for topic: String) -> AgreementKeys?
func getSymmetricKey(for topic: String) -> SymmetricKey?
func getSymmetricKeyRepresentable(for topic: String) -> Data?
func getPublicKey(for topic: String) -> AgreementPublicKey?
func deletePrivateKey(for publicKey: String)
func deleteAgreementSecret(for topic: String)
func deleteSymmetricKey(for topic: String)
Expand All @@ -18,7 +20,6 @@ public protocol KeyManagementServiceProtocol {
}

public class KeyManagementService: KeyManagementServiceProtocol {

enum Error: Swift.Error {
case keyNotFound
}
Expand Down Expand Up @@ -52,12 +53,17 @@ public class KeyManagementService: KeyManagementServiceProtocol {
public func setPrivateKey(_ privateKey: AgreementPrivateKey) throws {
try keychain.add(privateKey, forKey: privateKey.publicKey.hexRepresentation)
}


public func setPublicKey(publicKey: AgreementPublicKey, for topic: String) throws {
try keychain.add(publicKey, forKey: topic)
}


public func setAgreementSecret(_ agreementSecret: AgreementKeys, topic: String) throws {
try keychain.add(agreementSecret, forKey: topic)
}

public func getSymmetricKey(for topic: String) throws -> SymmetricKey? {
public func getSymmetricKey(for topic: String) -> SymmetricKey? {
do {
return try keychain.read(key: topic) as SymmetricKey
} catch {
Expand All @@ -66,10 +72,10 @@ public class KeyManagementService: KeyManagementServiceProtocol {
}

public func getSymmetricKeyRepresentable(for topic: String) -> Data? {
if let key = try? getAgreementSecret(for: topic)?.sharedKey {
if let key = getAgreementSecret(for: topic)?.sharedKey {
return key.rawRepresentation
} else {
return try? getSymmetricKey(for: topic)?.rawRepresentation
return getSymmetricKey(for: topic)?.rawRepresentation
}
}

Expand All @@ -83,13 +89,21 @@ public class KeyManagementService: KeyManagementServiceProtocol {
}
}

public func getAgreementSecret(for topic: String) throws -> AgreementKeys? {
public func getAgreementSecret(for topic: String) -> AgreementKeys? {
do {
return try keychain.read(key: topic) as AgreementKeys
} catch {
return nil
}
}

public func getPublicKey(for topic: String) -> AgreementPublicKey? {
do {
return try keychain.read(key: topic) as AgreementPublicKey
} catch {
return nil
}
}

public func deletePrivateKey(for publicKey: String) {
do {
Expand Down
74 changes: 74 additions & 0 deletions Sources/WalletConnectKMS/Serialiser/Envelope.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Foundation
import WalletConnectUtils

/// A type representing envelope with it's serialization policy
public struct Envelope: Equatable {
enum Errors: String, Error {
case malformedEnvelope
case unsupportedEnvelopeType
}

let type: EnvelopeType
let sealbox: Data

/// - Parameter base64encoded: base64encoded envelope
/// tp = type byte (1 byte)
/// pk = public key (32 bytes)
/// iv = initialization vector (12 bytes)
/// ct = ciphertext (N bytes)
/// sealbox = iv + ct + tag
/// type0: tp + sealbox
/// type1: tp + pk + sealbox
init(_ base64encoded: String) throws {
guard let envelopeData = Data(base64Encoded: base64encoded) else {
throw Errors.malformedEnvelope
}
let envelopeTypeByte = envelopeData.subdata(in: 0..<1).uint8
if envelopeTypeByte == 0 {
self.type = .type0
self.sealbox = envelopeData.subdata(in: 1..<envelopeData.count)
} else if envelopeTypeByte == 1 {
let pubKey = envelopeData.subdata(in: 1..<33)
flypaper0 marked this conversation as resolved.
Show resolved Hide resolved
self.type = .type1(pubKey: pubKey)
self.sealbox = envelopeData.subdata(in: 33..<envelopeData.count)
} else {
throw Errors.unsupportedEnvelopeType
}
}

init(type: EnvelopeType, sealbox: Data) {
self.type = type
self.sealbox = sealbox
}

func serialised() -> String {
switch type {
case .type0:
return (type.representingByte.data + sealbox).base64EncodedString()
case .type1(let pubKey):
return (type.representingByte.data + pubKey + sealbox).base64EncodedString()
}
}

}

public extension Envelope {
enum EnvelopeType: Equatable {
enum Errors: Error {
case unsupportedPolicyType
llbartekll marked this conversation as resolved.
Show resolved Hide resolved
}
/// type 0 = tp + iv + ct + tag
case type0
/// type 1 = tp + pk + iv + ct + tag
case type1(pubKey: Data)

var representingByte: UInt8 {
switch self {
case .type0:
return 0
case .type1:
return 1
}
}
}
}
59 changes: 40 additions & 19 deletions Sources/WalletConnectKMS/Serialiser/Serializer.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import Foundation
import WalletConnectUtils


public class Serializer {
enum Error: String, Swift.Error {
enum Errors: String, Error {
case symmetricKeyForTopicNotFound
}

private let kms: KeyManagementServiceProtocol
private let codec: Codec

Expand All @@ -22,38 +22,59 @@ public class Serializer {
/// Encrypts and serializes an object
/// - Parameters:
/// - topic: Topic that is associated with a symetric key for encrypting particular codable object
/// - message: Message to encrypt and serialize
/// - encodable: Object to encrypt and serialize
/// - envelopeType: type of envelope
/// - Returns: Serialized String
public func serialize(topic: String, encodable: Encodable) throws -> String {
public func serialize(topic: String, encodable: Encodable, envelopeType: Envelope.EnvelopeType) throws -> String {
let messageJson = try encodable.json()
if let symmetricKey = kms.getSymmetricKeyRepresentable(for: topic) {
return try codec.encode(plaintext: messageJson, symmetricKey: symmetricKey)
} else {
throw Error.symmetricKeyForTopicNotFound
guard let symmetricKey = kms.getSymmetricKeyRepresentable(for: topic) else {
throw Errors.symmetricKeyForTopicNotFound
}
let sealbox = try codec.encode(plaintext: messageJson, symmetricKey: symmetricKey)
return Envelope(type: envelopeType, sealbox: sealbox).serialised()
}

/// Deserializes and decrypts an object
/// - Parameters:
/// - topic: Topic that is associated with a symetric key for decrypting particular codable object
/// - message: Message to deserialize and decrypt
/// - encodedEnvelope: Envelope to deserialize and decrypt
/// - Returns: Deserialized object
public func tryDeserialize<T: Codable>(topic: String, message: String) -> T? {
public func tryDeserialize<T: Codable>(topic: String, encodedEnvelope: String) -> T? {
do {
let deserializedCodable: T
if let symmetricKey = kms.getSymmetricKeyRepresentable(for: topic) {
return try deserialize(message: message, symmetricKey: symmetricKey)
} else {
throw Error.symmetricKeyForTopicNotFound
let envelope = try Envelope(encodedEnvelope)
flypaper0 marked this conversation as resolved.
Show resolved Hide resolved
switch envelope.type {
case .type0:
let decodedType: T? = try handleType0Envelope(topic, envelope)
return decodedType
case .type1:
let decodedType: T? = try handleType1Envelope(topic, envelope)
return decodedType
}
} catch {
return nil
}
}

private func handleType0Envelope<T: Codable>(_ topic: String, _ envelope: Envelope) throws -> T? {
if let symmetricKey = kms.getSymmetricKeyRepresentable(for: topic) {
return try decode(sealbox: envelope.sealbox, symmetricKey: symmetricKey)
} else {
throw Errors.symmetricKeyForTopicNotFound
}
}

private func handleType1Envelope<T: Codable>(_ topic: String, _ envelope: Envelope) throws -> T? {
guard let pubKey = kms.getPublicKey(for: topic),
case let .type1(peerPubKey) = envelope.type else { return nil }
let agreementKeys = try kms.performKeyAgreement(selfPublicKey: pubKey, peerPublicKey: peerPubKey.toHexString())
let decodedType: T? = try decode(sealbox: envelope.sealbox, symmetricKey: agreementKeys.sharedKey.rawRepresentation)
let newTopic = agreementKeys.derivedTopic()
try kms.setAgreementSecret(agreementKeys, topic: newTopic)
return decodedType
}

private func deserialize<T: Codable>(message: String, symmetricKey: Data) throws -> T {
let decryptedData = try codec.decode(sealboxString: message, symmetricKey: symmetricKey)
private func decode<T: Codable>(sealbox: Data, symmetricKey: Data) throws -> T {
let decryptedData = try codec.decode(sealbox: sealbox, symmetricKey: symmetricKey)
return try JSONDecoder().decode(T.self, from: decryptedData)
}
}

Loading