Skip to content

Commit

Permalink
Merge pull request #313 from tangem/IOS-4136_compatible_ed
Browse files Browse the repository at this point in the history
IOS-4136 Add ed25519 full support
  • Loading branch information
tureck1y authored Aug 8, 2023
2 parents a726dd6 + 6579315 commit e2b8b02
Show file tree
Hide file tree
Showing 36 changed files with 1,571 additions and 315 deletions.
30 changes: 28 additions & 2 deletions Example/TangemSdkExample/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ class AppModel: ObservableObject {
} else {
config.logConfig = .verbose
}

config.defaultDerivationPaths = [
.secp256k1: [try! DerivationPath(rawPath: "m/0'/1")],
.secp256r1: [try! DerivationPath(rawPath: "m/0'/1")],
.ed25519: [try! DerivationPath(rawPath: "m/0'/1")],
.ed25519_slip0010: [try! DerivationPath(rawPath: "m/0'/1'")],
.bip0340: [try! DerivationPath(rawPath: "m/0'/1")]
]
_tangemSdk.config = config
return _tangemSdk
}
Expand Down Expand Up @@ -92,6 +100,10 @@ class AppModel: ObservableObject {
func copy() {
UIPasteboard.general.string = logText
}

func hideKeyboard() {
UIApplication.shared.endEditing()
}

func start(walletPublicKey: Data? = nil) {
isScanning = true
Expand Down Expand Up @@ -239,6 +251,8 @@ extension AppModel {
self.complete(with: "Scan card before")
return
}

let verifyKey = (path.flatMap { wallet.derivedKeys[$0] })?.publicKey ?? walletPublicKey

let hashSize = wallet.curve == .ed25519 ? 64 : 32
let hash = getRandomHash(size: hashSize)
Expand All @@ -247,8 +261,20 @@ extension AppModel {
walletPublicKey: walletPublicKey,
cardId: nil,
derivationPath: path,
initialMessage: Message(header: "Signing hash"),
completion: handleCompletion)
initialMessage: Message(header: "Signing hash")) { result in

if case .success(let response) = result {
if #available(iOS 16.0, *), wallet.curve == .secp256r1 {
let isValid = try? CryptoUtils.verifySecp256r1Signature(publicKey: verifyKey, hash: hash, signature: response.signature)
self.logger.log("signature status: \(String(describing: isValid))")
} else {
let isValid = try? CryptoUtils.verify(curve: wallet.curve, publicKey: verifyKey, hash: hash, signature: response.signature)
self.logger.log("signature status: \(String(describing: isValid))")
}
}

self.handleCompletion(result)
}
}

func signHashes(walletPublicKey: Data) {
Expand Down
3 changes: 2 additions & 1 deletion Example/TangemSdkExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ struct ContentView: View {
Button("Copy", action: model.copy)
Button("Backup", action: model.onBackup)
Button("Reset", action: model.onResetService)
Button("Hide kb", action: model.hideKeyboard)
}

additionalView
Expand Down Expand Up @@ -139,7 +140,7 @@ struct ContentView: View {
.tag(supportedCurves[index])
}
}
.pickerStyle(SegmentedPickerStyle())
.pickerStyle(WheelPickerStyle())
}

if case .importWallet = model.method {
Expand Down
58 changes: 53 additions & 5 deletions TangemSdk/TangemSdk.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion TangemSdk/TangemSdk/Common/Card/EllipticCurve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Foundation
public enum EllipticCurve: String, StringCodable, CaseIterable {
case secp256k1
case ed25519
case ed25519_slip0010
case secp256r1
case bls12381_G2
case bls12381_G2_AUG
Expand All @@ -24,7 +25,7 @@ public enum EllipticCurve: String, StringCodable, CaseIterable {
extension EllipticCurve {
public var supportsDerivation: Bool {
switch self {
case .secp256k1, .ed25519, .secp256r1, .bip0340:
case .secp256k1, .ed25519, .ed25519_slip0010, .secp256r1, .bip0340:
return true
default:
return false
Expand Down
2 changes: 2 additions & 0 deletions TangemSdk/TangemSdk/Common/Card/FirmwareVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ public extension FirmwareVersion { //todo: move all doubleValue checks to consta
static let keysImportAvailable = FirmwareVersion(major: 6, minor: 21)
/// Tmp range for visa cards
static let visaRange = 5.25...5.30
/// ed25519_slip0010
static let ed25519Slip0010Available = FirmwareVersion(major: 6, minor: 32)
}

@available(iOS 13.0, *)
Expand Down
2 changes: 2 additions & 0 deletions TangemSdk/TangemSdk/Common/Core/TangemSdkError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public enum TangemSdkError: Error, LocalizedError, Encodable {

// Read Errors
case accessCodeRequired
case nonHardenedDerivationNotSupported

// CreateWallet Errors
case alreadyCreated
Expand Down Expand Up @@ -347,6 +348,7 @@ public enum TangemSdkError: Error, LocalizedError, Encodable {
case .cannotBeDepersonalized: return 40201

case .accessCodeRequired: return 40401
case .nonHardenedDerivationNotSupported: return 40402
case .walletCannotBeCreated: return 40403
case .cardWithMaxZeroWallets: return 40404
case .walletAlreadyCreated: return 40405
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ struct CardDeserializer {
if fwVersion < .blsAvailable {
return [.secp256k1, .ed25519, .secp256r1]
}

if fwVersion < .ed25519Slip0010Available {
return [.secp256k1, .ed25519, .secp256r1, .bls12381_G2, .bls12381_G2_AUG, .bls12381_G2_POP]
}

return EllipticCurve.allCases
}
Expand Down
17 changes: 5 additions & 12 deletions TangemSdk/TangemSdk/Common/JSON/Handlers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,13 @@ class ImportWalletHandler: JSONRPCHandler {

func makeRunnable(from parameters: [String : Any]) throws -> AnyJSONRPCRunnable {
let curve: EllipticCurve = try parameters.value(for: "curve")

let seedParam: Data? = try parameters.value(for: "seed")

let mnemonicString: String? = try parameters.value(for: "mnemonic")
let mnemonicString: String = try parameters.value(for: "mnemonic")
let passphrase: String = try parameters.value(for: "passphrase") ?? ""
let seedFromMnemonic = try mnemonicString.map { try Mnemonic(with: $0).generateSeed(with: passphrase) }

let seed: Data? = seedParam ?? seedFromMnemonic
guard let seed else {
throw JSONRPCError(.invalidParams,
data: JSONRPCErrorData(.invalidParams, message: "You should pass a seed or a mnemonic and an optional passphrase"))
}

let command = CreateWalletTask(curve: curve, seed: seed)
let mnemonic = try Mnemonic(with: mnemonicString)
let factory = AnyMasterKeyFactory(mnemonic: mnemonic, passphrase: passphrase)
let privateKey = try factory.makeMasterKey(for: curve)
let command = CreateWalletTask(curve: curve, privateKey: privateKey)
return command.eraseToAnyRunnable()
}
}
Expand Down
22 changes: 0 additions & 22 deletions TangemSdk/TangemSdk/Common/MasterKeyFactory.swift

This file was deleted.

33 changes: 30 additions & 3 deletions TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public struct BIP39 {
}

// Validate wordlist by the first word
let wordlistDictionary = try getWordlist(by: mnemonicComponents[0]).dictionary
let wordlist = try getWordlist(by: mnemonicComponents[0]).dictionary
let wordlistDictionary = Dictionary(uniqueKeysWithValues: wordlist.enumerated().map { ($1, $0) })

// Validate all the words
var invalidWords = Set<String>()
Expand All @@ -36,7 +37,7 @@ public struct BIP39 {
var concatenatedBits = ""

for word in mnemonicComponents {
guard let wordIndex = wordlistDictionary.firstIndex(of: word) else {
guard let wordIndex = wordlistDictionary[word] else {
invalidWords.insert(word)
continue
}
Expand All @@ -61,7 +62,7 @@ public struct BIP39 {
let checksumBits = String(concatenatedBits.suffix(checksumBitsCount))

guard let entropyData = Data(bitsString: entropyBits) else {
throw MnemonicError.invalidCheksum
throw MnemonicError.invalidMnemonic
}

let calculatedChecksumBits = entropyData
Expand Down Expand Up @@ -176,6 +177,32 @@ public struct BIP39 {
return mnemonicComponents.joined(separator: " ")
}

/// Calculate initial entropy from mnemonic components.
/// - Parameter mnemonicComponents: Menemonic components to use
/// - Returns: The initial entropy
func getEntropy(from mnemonicComponents: [String]) throws -> Data {
let wordlist = try getWordlist(by: mnemonicComponents[0]).dictionary
let wordlistDictionary = Dictionary(uniqueKeysWithValues: wordlist.enumerated().map { ($1, $0) })

let concatenatedBits = try mnemonicComponents.map {
guard let wordIndex = wordlistDictionary[$0] else {
throw MnemonicError.invalidMnemonic
}

return String(wordIndex, radix: 2).leadingZeroPadding(toLength: 11)
}.joined()

let checksumBitsCount = mnemonicComponents.count / 3
let entropyBitsCount = concatenatedBits.count - checksumBitsCount
let entropyBits = String(concatenatedBits.prefix(entropyBitsCount))

guard let entropyData = Data(bitsString: entropyBits) else {
throw MnemonicError.invalidMnemonic
}

return entropyData
}

private func normalizedData(from string: String) throws -> Data {
let normalizedString = string.decomposedStringWithCompatibilityMapping

Expand Down
6 changes: 6 additions & 0 deletions TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,10 @@ public struct Mnemonic {
public func generateSeed(with passphrase: String = "") throws -> Data {
return try bip39.generateSeed(from: mnemonicComponents, passphrase: passphrase)
}

/// Returns initial entropy
/// - Returns: entropy data
public func getEntropy() throws -> Data {
return try bip39.getEntropy(from: mnemonicComponents)
}
}
1 change: 1 addition & 0 deletions TangemSdk/TangemSdk/Crypto/BIP39/MnemonicError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public enum MnemonicError: Error {
case invalidWordCount
case invalidWordsFile
case invalidCheksum
case invalidMnemonic
case mnenmonicCreationFailed
case normalizationFailed
case wrongWordCount
Expand Down
31 changes: 27 additions & 4 deletions TangemSdk/TangemSdk/Crypto/CryptoUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public enum CryptoUtils {
case .bip0340:
let signature = try SchnorrSignature(with: signature)
return try signature.verify(with: publicKey, message: message)
case .ed25519:
case .ed25519, .ed25519_slip0010:
let hash = message.getSha512()
let pubKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKey)
return pubKey.isValidSignature(signature, for: hash)
Expand All @@ -70,7 +70,12 @@ public enum CryptoUtils {
switch curve {
case .secp256k1, .bip0340:
return Secp256k1Utils().isPrivateKeyValid(privateKey)
case .ed25519:
case .ed25519, .ed25519_slip0010:
// Extended private keys not supported by CryptoKit
if privateKey.count > Constants.ed25519PrivateKeySize {
throw TangemSdkError.unsupportedCurve
}

let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: privateKey)
return key != nil
case .secp256r1:
Expand All @@ -89,7 +94,12 @@ public enum CryptoUtils {
return try Secp256k1Utils().createPublicKey(privateKey: privateKey, compressed: true)
case .bip0340:
return try Secp256k1Utils().createXOnlyPublicKey(privateKey: privateKey)
case .ed25519:
case .ed25519, .ed25519_slip0010:
// Extended private keys not supported by CryptoKit
if privateKey.count > Constants.ed25519PrivateKeySize {
throw TangemSdkError.unsupportedCurve
}

let key = try Curve25519.Signing.PrivateKey(rawRepresentation: privateKey)
return key.publicKey.rawRepresentation
case .secp256r1:
Expand Down Expand Up @@ -117,7 +127,7 @@ public enum CryptoUtils {
case .bip0340:
let signature = try SchnorrSignature(with: signature)
return try signature.verify(with: publicKey, hash: hash)
case .ed25519:
case .ed25519, .ed25519_slip0010:
let pubKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKey)
return pubKey.isValidSignature(signature, for: hash)
case .secp256r1:
Expand All @@ -135,6 +145,18 @@ public enum CryptoUtils {
}
}

/// Verify secp256r1 signature
@available(iOS 16.0, *)
public static func verifySecp256r1Signature(publicKey: Data, hash: Data, signature: Data) throws -> Bool {
if publicKey.count == Constants.p256CompressedKeySize {
let pubKey = try P256.Signing.PublicKey(compressedRepresentation: publicKey)
let sig = try P256.Signing.ECDSASignature(rawRepresentation: signature)
return pubKey.isValidSignature(sig, for: CustomSha256Digest(hash: hash))
}

return try verify(curve: .secp256r1, publicKey: publicKey, hash: hash, signature: signature)
}

public static func crypt(operation: Int, algorithm: Int, options: Int, key: Data, dataIn: Data) throws -> Data {
return try key.withUnsafeBytes { keyUnsafeRawBufferPointer in
return try dataIn.withUnsafeBytes { dataInUnsafeRawBufferPointer in
Expand Down Expand Up @@ -173,5 +195,6 @@ fileprivate struct CustomSha256Digest: Digest {
private extension CryptoUtils {
enum Constants {
static let p256CompressedKeySize = 33
static let ed25519PrivateKeySize = 32
}
}
21 changes: 16 additions & 5 deletions TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/BIP32.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@ public struct BIP32 {
/// - curve: The curve to use
/// - Returns: The `ExtendedPrivateKey`
public func makeMasterKey(from seed: Data, curve: EllipticCurve) throws -> ExtendedPrivateKey {
guard let hmacKey = curve.hmacKey else {
throw TangemSdkError.unsupportedCurve
}

// The seed must be between 128 and 512 bits
guard 16...64 ~= seed.count else {
throw HDWalletError.invalidSeed
}

guard let keyData = curve.hmacKey.rawValue.data(using: .utf8) else {
guard let keyData = hmacKey.rawValue.data(using: .utf8) else {
throw HDWalletError.invalidHMACKey
}

Expand All @@ -37,7 +41,7 @@ public struct BIP32 {

// Verify the key
// https://github.com/satoshilabs/slips/blob/master/slip-0010.md
if curve != .ed25519, !(try CryptoUtils.isPrivateKeyValid(iL, curve: curve)) {
if curve != .ed25519_slip0010, !(try CryptoUtils.isPrivateKeyValid(iL, curve: curve)) {
return try makeMasterKey(from: i, curve: curve)
}

Expand All @@ -63,17 +67,24 @@ extension BIP32 {

@available(iOS 13.0, *)
fileprivate extension EllipticCurve {
var hmacKey: BIP32.HMACKey {
var hmacKey: BIP32.HMACKey? {
switch self {
case .secp256k1, .bip0340:
return .secp256k1
case .ed25519:
case .ed25519_slip0010:
return .ed25519
case .secp256r1:
return .secp256r1
case .ed25519:
// we use ikarus master key generation scheme for this curve
// https://github.com/satoshilabs/slips/blob/master/slip-0023.md
assertionFailure("not applicable for this curve")
return nil
case .bls12381_G2, .bls12381_G2_AUG, .bls12381_G2_POP:
// Use BLSUtils.generateKey instead
// https://eips.ethereum.org/EIPS/eip-2333#derive_master_sk
fatalError("not applicable for this curve")
assertionFailure("not applicable for this curve")
return nil
}
}
}
Loading

0 comments on commit e2b8b02

Please sign in to comment.