Skip to content

Commit

Permalink
Merge pull request #1 from swift-libp2p/dev/pem+api
Browse files Browse the repository at this point in the history
PEM Imports and Exports + Code Refactoring
  • Loading branch information
btoms20 authored Sep 23, 2022
2 parents 6d44cf2 + 60876db commit 7f85efe
Show file tree
Hide file tree
Showing 9 changed files with 413 additions and 226 deletions.
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ let package = Package(
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/swift-libp2p/swift-libp2p-crypto.git", .upToNextMajor(from: "0.0.1")),
.package(url: "https://github.com/swift-libp2p/swift-libp2p-crypto.git", .upToNextMinor(from: "0.1.1")),
.package(url: "https://github.com/swift-libp2p/swift-multihash.git", .upToNextMajor(from: "0.0.1")),
.package(url: "https://github.com/swift-libp2p/swift-cid.git", .upToNextMajor(from: "0.0.1")),
.package(url: "https://github.com/apple/swift-protobuf.git", .upToNextMajor(from: "1.12.0"))
Expand All @@ -29,8 +29,8 @@ let package = Package(
name: "PeerID",
dependencies: [
.product(name: "LibP2PCrypto", package: "swift-libp2p-crypto"),
.product(name: "CID", package: "swift-cid"),
.product(name: "Multihash", package: "swift-multihash"),
.product(name: "CID", package: "swift-cid"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
],
resources: [
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ peerID.keyPair?.keyType == .ed25519 // The type of Key
peerID.keyPair?.privateKey // Access to the private key (for signing)
peerID.keyPair?.publicKey // Access to the public key (for verifying signatures)

/// If you want to reuse the same PeerID between sessions, you can...

/// Export a PeerID as an Encrypted PEM String that you can store...
let encryptedPEM = try peerID.exportKeyPair(as: .privatePEMString(encryptedWithPassword: "mypassword"))

/// And then load the PeerID from and encrypted PEM String later
let peerID = try PeerID(pem: "ENCRYPTED_PEM_String", password: "mypassword")
```

### API
Expand Down Expand Up @@ -108,6 +115,8 @@ PeerID.init(marshaledPrivateKey str:String, base:BaseEncoding) throws
/// Inits a `PeerID` from a marshaled private key
PeerID.init(marshaledPrivateKey data:Data) throws

/// Inits a `PeerID` from a PEM String
PeerID.init(pem: String, withPassword: String? = nil) throws

/// Properties
/// Returns the PeerID's id as a base58 string (multihash/CIDv0).
Expand Down Expand Up @@ -136,6 +145,8 @@ PeerID.toJSON(includingPrivateKey:Bool = false) throws -> Data
/// Exports our PeerID as a JSON string
PeerID.toJSONString(includingPrivateKey:Bool = false) throws -> String?

/// Exports our PeerID as a PEM String
PeerID.exportKeyPair(as: PeerID.ExportType) throws -> String

/// Signing and Verifying
// Signs data using this PeerID's private key. This signature can then be verified by a remote peer using this PeerID's public key
Expand Down
20 changes: 20 additions & 0 deletions Sources/PeerID/PeerID+Equatable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// PeerID+Equatable.swift
//
//
// Created by Brandon Toms on 9/23/22.
//

import Foundation

extension PeerID:Equatable {
public static func == (lhs: PeerID, rhs: PeerID) -> Bool {
lhs.id == rhs.id
}
public static func == (lhs: [UInt8], rhs: PeerID) -> Bool {
lhs == rhs.id
}
public static func == (lhs: Data, rhs: PeerID) -> Bool {
lhs.bytes == rhs.id
}
}
86 changes: 86 additions & 0 deletions Sources/PeerID/PeerID+JSON.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//
// PeerID+JSON.swift
//
//
// Created by Brandon Toms on 9/23/22.
//

import LibP2PCrypto
import Foundation
import Multihash

/// - MARK: JSON Imports and Exports
public extension PeerID {
/// An Internal PeerID struct to facilitate JSON Encoding and Decoding
internal struct PeerIDJSON:Codable {
/// base58 encoded string
let id:String
/// base64 encoded publicKey protobuf
let pubKey:String?
/// base64 encoded privateKey protobuf
let privKey:String?
}

/// Initialize a PeerID from JSON data
///
/// Expects a JSON object of the form
/// ```
/// {
/// obj.id: String - The multihash encoded in base58
/// obj.pubKey: String? - The public key in protobuf format, encoded in 'base64'
/// obj.privKey: String? - The private key in protobuf format, encoded in 'base64'
/// }
/// ```
convenience init(fromJSON json:Data) throws {
let data = try JSONDecoder().decode(PeerIDJSON.self, from: json)

if data.privKey == nil && data.pubKey == nil {
/// Only ID Present...
try self.init(fromBytesID: Multihash(b58String: data.id).value)
} else if data.privKey == nil, let pubKey = data.pubKey {
/// Only Public Key and ID Present, lets init via the public key and derive the ID
/// TODO: Compare the provided ID and the Derived ID and throw an error if they dont match...
try self.init(marshaledPublicKey: pubKey, base: .base64)
} else if let privKey = data.privKey {
/// Private Key was provided. Lets init via the private key and derive both the public key and the ID
/// TODO: Compare the provided publicKey and ID to the ones derived from the private key and throw an error if they don't match...
try self.init(marshaledPrivateKey: privKey, base: .base64)
} else {
throw NSError(domain: "Failed to init PeerID from json", code: 0, userInfo: nil)
}
}

/// Exports our PeerID as a JSON object
///
/// Returns a JSON object of the form
/// ```
/// {
/// id: String - The multihash encoded in base58
/// pubKey: String? - The public key in protobuf format, encoded in 'base64'
/// privKey: String? - The private key in protobuf format, encoded in 'base64'
/// }
/// ```
func toJSON(includingPrivateKey:Bool = false) throws -> Data {
let pidJSON = PeerIDJSON(
id: self.b58String,
pubKey: try? self.keyPair?.publicKey.marshal().asString(base: .base64),
privKey: includingPrivateKey ? try? self.keyPair?.privateKey?.marshal().asString(base: .base64) : nil
)

return try JSONEncoder().encode(pidJSON)
}

/// Exports our PeerID as a JSON object
///
/// Returns a JSON object as a String
/// ```
/// {
/// id: String - The multihash encoded in base58
/// pubKey: String? - The public key in protobuf format, encoded in 'base64'
/// privKey: String? - The private key in protobuf format, encoded in 'base64'
/// }
/// ```
func toJSONString(includingPrivateKey:Bool = false) throws -> String? {
return try String(data: self.toJSON(), encoding: .utf8)
}
}
118 changes: 118 additions & 0 deletions Sources/PeerID/PeerID+Marshaled.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//
// PeerID+Marshaled.swift
//
//
// Created by Brandon Toms on 9/23/22.
//

import LibP2PCrypto
import Foundation
import Multibase

/// - MARK: Marshaled Imports and Exports
public extension PeerID {
/// Inits a `PeerID` from a marshaled `PeerID` string
/// - Note: `base` can be left `nil` if the marshaledPeerID String is `Multibase` compliant (includes the multibase prefix) otherwise, you must specify the ecoded base of the string...
convenience init(marshaledPeerID:String, base: BaseEncoding? = nil) throws {
let marshaledData:Data
if let base = base {
marshaledData = try BaseEncoding.decode(marshaledPeerID, as: base).data
} else {
marshaledData = try BaseEncoding.decode(marshaledPeerID).data
}
try self.init(marshaledPeerID: marshaledData)
}

/// Inits a `PeerID` from a marshaled `PeerID`
convenience init(marshaledPeerID data:Data) throws {
// Attampt to instantiate a PeerIdProto with the raw, marshaled, data
let protoPeerID = try PeerIdProto(contiguousBytes: data)

//print(protoPeerID.id.asString(base: .base64))
//print("Has PubKey: \(protoPeerID.hasPubKey)")
//print(protoPeerID.pubKey.asString(base: .base64))
//print("Has PrivKey: \(protoPeerID.hasPrivKey)")
//print(protoPeerID.privKey.asString(base: .base64))

// Enusre the Marshaled data included at least a public key (is this necessary, would we ever need to unmarshal an ID only?)
guard protoPeerID.hasPubKey || protoPeerID.hasPrivKey else {
throw NSError(domain: "No Public or Private Key Found in marshaled data", code: 0, userInfo: nil)
}

// If we have a private key, attempt to instantiate the PeerID via the private key, otherwise, try the public key...
if protoPeerID.hasPrivKey {
try self.init(marshaledPrivateKey: protoPeerID.privKey)
} else if protoPeerID.hasPubKey {
try self.init(marshaledPublicKey: protoPeerID.pubKey)
} else {
throw NSError(domain: "No Public or Private Key Found in marshaled data", code: 0, userInfo: nil)
}
}

/// Inits a `PeerID` from a marshaled public key string
convenience init(marshaledPublicKey str:String, base:BaseEncoding) throws {
try self.init(keyPair: LibP2PCrypto.Keys.KeyPair(marshaledPublicKey: str, base: base))
}

/// Inits a `PeerID` from a marshaled public key
convenience init(marshaledPublicKey key:Data) throws {
try self.init(keyPair: LibP2PCrypto.Keys.KeyPair(marshaledPublicKey: key))
}

/// Inits a `PeerID` from a marshaled private key string
convenience init(marshaledPrivateKey str:String, base:BaseEncoding) throws {
try self.init(keyPair: LibP2PCrypto.Keys.KeyPair(marshaledPrivateKey: str, base: base))
}

/// Inits a `PeerID` from a marshaled private key
convenience init(marshaledPrivateKey data:Data) throws {
try self.init(keyPair: LibP2PCrypto.Keys.KeyPair(marshaledPrivateKey: data))
}


// private static func computeDigest(pubKey:SecKey) throws -> [UInt8] {
// let bytes = try pubKey.rawRepresentation()
// return try self.computeDigest(rawPubKey: bytes)
// }
//
// /// - Note: We need to marshal the raw public key before multihashing it....
// private static func computeDigest(rawPubKey bytes:Data) throws -> [UInt8] {
// let marshaled = try LibP2PCrypto.Keys.marshalPublicKey(raw: bytes, keyType: .RSA(bits: .B1024))
// //print(marshaled.asString(base: .base64Pad))
// if marshaled.count <= 42 {
// return try Multihash(raw: marshaled, hashedWith: .identity).value
// } else {
// //let mh = try Multihash(raw: bytes, hashedWith: .sha2_256)
// //print("Value: \(mh.value.asString(base: .base16))")
// //print("Hex: \(mh.hexString)")
// //print("Digest: \(mh.digest?.asString(base: .base16) ?? "NIL")")
// return try Multihash(raw: marshaled, hashedWith: .sha2_256).value //pubKey.hash()
// }
// }


/// Returns a protocol-buffers encoded version of the id, public key and, if `includingPrivateKey` is set to `true`, the private key.
func marshal(includingPrivateKey:Bool = false) throws -> [UInt8] {
var pid = PeerIdProto()
pid.id = Data(self.id)
pid.pubKey = try self.keyPair?.publicKey.marshal() ?? Data()
if includingPrivateKey, let privKey = self.keyPair?.privateKey {
pid.privKey = try privKey.marshal()
}
return try pid.serializedData().bytes
}

func marshalPrivateKey() throws -> [UInt8] {
guard let privKey = self.keyPair?.privateKey else {
throw NSError(domain: "This PeerID doesn't have a Private Key to Marshal", code: 0, userInfo: nil)
}
return try privKey.marshal().bytes
}

func marshalPublicKey() throws -> [UInt8] {
guard let pubKey = self.keyPair?.publicKey else {
throw NSError(domain: "This PeerID doesn't have a Public Key to Marshal", code: 0, userInfo: nil)
}
return try pubKey.marshal().bytes
}
}
37 changes: 37 additions & 0 deletions Sources/PeerID/PeerID+PEM.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// PeerID+PEM.swift
//
//
// Created by Brandon Toms on 9/23/22.
//

import LibP2PCrypto
import Foundation

/// - MARK: PEM Imports and Exports
public extension PeerID {
convenience init(pem: String, password: String?) throws {
try self.init(keyPair: LibP2PCrypto.Keys.KeyPair(pem: pem, password: password))
}

enum ExportType {
case publicPEMString
case privatePEMString(encryptedWithPassword:String)
case unencrypredPrivatePEMString
}

/// Exports the KeyPair as PEM structured String. Private Keys can be encrypted with a password before export.
func exportKeyPair(as exportType:ExportType) throws -> String {
guard let keyPair = self.keyPair else { throw NSError(domain: "No Underlying Key Pair to Export", code: 0, userInfo: nil) }
switch exportType {
case .publicPEMString:
return try keyPair.publicKey.exportPublicKeyPEMString(withHeaderAndFooter: true)
case .unencrypredPrivatePEMString:
guard keyPair.privateKey != nil else { throw NSError(domain: "No Private Key to Export", code: 0, userInfo: nil) }
return try keyPair.exportPrivatePEMString(withHeaderAndFooter: true)
case .privatePEMString(let password):
guard !password.isEmpty else { throw NSError(domain: "Password shouldn't be empty", code: 0, userInfo: nil) }
return try keyPair.exportEncryptedPrivatePEMString(withPassword: password)
}
}
}
30 changes: 30 additions & 0 deletions Sources/PeerID/PeerID+Signatures.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// PeerID+Signature.swift
//
//
// Created by Brandon Toms on 9/23/22.
//

import LibP2PCrypto
import Foundation

/// - MARK: PeerID Signatures and Verification Methods
public extension PeerID {
// Signs data using this PeerID's private key. This signature can then be verified by a remote peer using this PeerID's public key
func signature(for msg:Data) throws -> Data {
guard let priv = keyPair?.privateKey else {
throw NSError(domain: "A private key is required for generating signature and this PeerID doesn't contain a private key.", code: 0, userInfo: nil)
}

return try priv.sign(message: msg)
}

// Using this PeerID's public key, this method checks to see if the signature data was in fact signed by this peer and is a valid signature for the expected data
func isValidSignature(_ signature:Data, for expectedData:Data) throws -> Bool {
guard let pub = keyPair?.publicKey else {
throw NSError(domain: "A public key is required for verifying signatures and this PeerID doesn't contain a public key.", code: 0, userInfo: nil)
}

return try pub.verify(signature: signature, for: expectedData)
}
}
Loading

0 comments on commit 7f85efe

Please sign in to comment.