Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ let package = Package(
dependencies: baseDependencies,
plugins: basePlugins
),
.target(
name: "Cryptography",
dependencies: baseDependencies + [
.product(name: "secp256k1", package: "swift-secp256k1")
],
plugins: basePlugins
),
//
// MARK: - Tests
//
Expand Down Expand Up @@ -232,6 +239,14 @@ let package = Package(
],
plugins: basePlugins
),
.testTarget(
name: "CryptographyTests",
dependencies: [
"Cryptography",
"TestsUtils"
],
plugins: basePlugins
),
.target(
name: "TestsUtils",
dependencies: [
Expand Down
133 changes: 133 additions & 0 deletions Sources/Cryptography/Ed25519Strategy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import CryptoKit
import Foundation
import Utils

public enum KeyEncoding: String, Sendable {
case base58
case hex
}

public struct Ed25519PublicKey: Sendable, Equatable {
public let bytes: String
public let encoding: KeyEncoding
public let keyType: String = "ed25519"

public init(bytes: String, encoding: KeyEncoding) {
self.bytes = bytes
self.encoding = encoding
}
}

public struct Ed25519Signature: Sendable, Equatable {
public let bytes: String
public let encoding: KeyEncoding
public let keyType: String = "ed25519"

public init(bytes: String, encoding: KeyEncoding) {
self.bytes = bytes
self.encoding = encoding
}
}

public enum Ed25519Error: Error, Equatable {
case invalidSeedLength(Int)
case invalidKeyLength(Int)
case signingFailed
case encodingFailed
}

public struct Ed25519Strategy: Sendable {
public init() {}

/// Derive a private key from a seed.
/// The seed must be at least 32 bytes. Only the first 32 bytes are used.
/// Returns the 32-byte private key (seed portion).
public func getPrivateKeyFromSeed(seed: Data) throws -> Data {
guard seed.count >= 32 else {
throw Ed25519Error.invalidSeedLength(seed.count)
}

let trimmedSeed = seed.prefix(32)
return Data(trimmedSeed)
}

/// Get the public key from a private key.
/// The private key can be 32 bytes (seed only) or 64 bytes (seed + public key).
/// Returns the 32-byte public key.
public func getPublicKey(privateKey: Data) throws -> Data {
let keyBytes: Data
if privateKey.count == 64 {
keyBytes = Data(privateKey.suffix(32))
} else if privateKey.count == 32 {
let signingKey = try Curve25519.Signing.PrivateKey(rawRepresentation: privateKey)
keyBytes = signingKey.publicKey.rawRepresentation
} else {
throw Ed25519Error.invalidKeyLength(privateKey.count)
}
return keyBytes
}

/// Sign a message with a private key using Ed25519.
/// The private key should be 32 bytes (seed).
/// Returns the 64-byte signature.
public func sign(privateKey: Data, message: Data) throws -> Data {
let keyData: Data
if privateKey.count == 64 {
keyData = Data(privateKey.prefix(32))
} else if privateKey.count == 32 {
keyData = privateKey
} else {
throw Ed25519Error.invalidKeyLength(privateKey.count)
}

let signingKey: Curve25519.Signing.PrivateKey
do {
signingKey = try Curve25519.Signing.PrivateKey(rawRepresentation: keyData)
} catch {
throw Ed25519Error.signingFailed
}

let signature: Data
do {
signature = try signingKey.signature(for: message)
} catch {
throw Ed25519Error.signingFailed
}

return signature
}

/// Format a public key with the specified encoding.
/// Default encoding is base58.
public func formatPublicKey(
publicKey: Data,
encoding: KeyEncoding = .base58
) throws -> Ed25519PublicKey {
let encodedBytes: String
switch encoding {
case .base58:
encodedBytes = try Base58.encode(publicKey)
case .hex:
encodedBytes = publicKey.map { String(format: "%02x", $0) }.joined()
}

return Ed25519PublicKey(bytes: encodedBytes, encoding: encoding)
}

/// Format a signature with the specified encoding.
/// Default encoding is base58.
public func formatSignature(
signature: Data,
encoding: KeyEncoding = .base58
) throws -> Ed25519Signature {
let encodedBytes: String
switch encoding {
case .base58:
encodedBytes = try Base58.encode(signature)
case .hex:
encodedBytes = signature.map { String(format: "%02x", $0) }.joined()
}

return Ed25519Signature(bytes: encodedBytes, encoding: encoding)
}
}
163 changes: 163 additions & 0 deletions Sources/Cryptography/Secp256k1Strategy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import CryptoKit
import Foundation
import secp256k1

public struct Secp256k1PublicKey: Sendable, Equatable {
public let bytes: String
public let encoding: KeyEncoding
public let keyType: String = "secp256k1"

public init(bytes: String, encoding: KeyEncoding) {
self.bytes = bytes
self.encoding = encoding
}
}

public struct Secp256k1Signature: Sendable, Equatable {
public let bytes: String
public let encoding: KeyEncoding
public let keyType: String = "secp256k1"

public init(bytes: String, encoding: KeyEncoding) {
self.bytes = bytes
self.encoding = encoding
}
}

public enum Secp256k1Error: Error, Equatable {
case invalidSeedLength(Int)
case invalidPrivateKey
case invalidDigestLength(Int)
case signingFailed
case encodingFailed
}

public struct Secp256k1Strategy: Sendable {
private static let derivationPath: [UInt8] = [
0x73, 0x65, 0x63, 0x70, 0x32, 0x35, 0x36, 0x6B, 0x31, 0x2D, 0x64, 0x65,
0x72, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2D, 0x70, 0x61, 0x74,
0x68
]

public init() {}

/// Derive a private key from a seed using secp256k1 derivation.
/// The seed is concatenated with the derivation path and hashed with SHA-256.
/// If the resulting key is invalid, the process is repeated recursively.
public func getPrivateKeyFromSeed(seed: Data) async throws -> Data {
var derivationSeed = Data(seed)
derivationSeed.append(contentsOf: Self.derivationPath)

let privateKeyData = Data(SHA256.hash(data: derivationSeed))

if isValidPrivateKey(privateKeyData) {
return privateKeyData
}

return try await getPrivateKeyFromSeed(seed: privateKeyData)
}

/// Check if a private key is valid for secp256k1.
private func isValidPrivateKey(_ keyData: Data) -> Bool {
guard keyData.count == 32 else { return false }

do {
_ = try secp256k1.Signing.PrivateKey(dataRepresentation: keyData, format: .uncompressed)
return true
} catch {
return false
}
}

/// Get the public key from a private key.
/// Returns the uncompressed public key (65 bytes).
public func getPublicKey(privateKey: Data) throws -> Data {
guard privateKey.count == 32 else {
throw Secp256k1Error.invalidPrivateKey
}

let signingKey: secp256k1.Signing.PrivateKey
do {
signingKey = try secp256k1.Signing.PrivateKey(dataRepresentation: privateKey, format: .uncompressed)
} catch {
throw Secp256k1Error.invalidPrivateKey
}

return signingKey.publicKey.dataRepresentation
}

/// Sign a 32-byte digest with a private key using secp256k1.
/// Returns the signature with recovery bit (r + s + v format, 65 bytes).
public func sign(privateKey: Data, digest: Data) throws -> Data {
guard digest.count == 32 else {
throw Secp256k1Error.invalidDigestLength(digest.count)
}

guard privateKey.count == 32 else {
throw Secp256k1Error.invalidPrivateKey
}

let ecdsaMemory = MemoryLayout<secp256k1_ecdsa_recoverable_signature>.size
guard let ecdsaMemoryStorage = malloc(ecdsaMemory) else {
throw Secp256k1Error.signingFailed
}

let signaturePointer = ecdsaMemoryStorage.assumingMemoryBound(
to: secp256k1_ecdsa_recoverable_signature.self
)

guard let context = secp256k1_context_create(
UInt32(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY)
) else {
free(signaturePointer)
throw Secp256k1Error.signingFailed
}

defer {
secp256k1_context_destroy(context)
free(signaturePointer)
}

var hash = [UInt8](digest)
let keyBytes = [UInt8](privateKey)

guard secp256k1_ecdsa_sign_recoverable(
context,
signaturePointer,
&hash,
keyBytes,
nil,
nil
) == 1 else {
throw Secp256k1Error.signingFailed
}

var signature = [UInt8](repeating: 0, count: 64)
var recid: Int32 = 0

secp256k1_ecdsa_recoverable_signature_serialize_compact(
context,
&signature,
&recid,
signaturePointer
)

let recoveryByte: UInt8 = recid == 1 ? 0x1C : 0x1B
var fullSignature = signature
fullSignature.append(recoveryByte)

return Data(fullSignature)
}

/// Format a public key as hex with 0x prefix.
public func formatPublicKey(publicKey: Data) -> Secp256k1PublicKey {
let hexString = "0x" + publicKey.map { String(format: "%02x", $0) }.joined()
return Secp256k1PublicKey(bytes: hexString, encoding: .hex)
}

/// Format a signature as hex with 0x prefix.
public func formatSignature(signature: Data) -> Secp256k1Signature {
let hexString = "0x" + signature.map { String(format: "%02x", $0) }.joined()
return Secp256k1Signature(bytes: hexString, encoding: .hex)
}
}
Loading
Loading