├── .gitignore ├── LICENSE ├── README.md ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── rn │ └── ecc │ ├── ECCModule.java │ └── ECCPackage.java ├── index.js ├── ios ├── RNECC.h ├── RNECC.m └── RNECC.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── tenaciousmv.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ └── tenaciousmv.xcuserdatad │ └── xcschemes │ ├── RNECC.xcscheme │ └── xcschememanagement.plist ├── package.json └── react-native-ecc.podspec /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | android/build 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tradle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-ecc 2 | 3 | basic elliptic curve crypto for React Native 4 | 5 | **this module is used by [Tradle](https://github.com/tradle/tim)** 6 | 7 | ## Installation 8 | 9 | See [Linking Libraries](http://facebook.github.io/react-native/docs/linking-libraries-ios.html) 10 | 11 | ## Usage 12 | 13 | ```js 14 | import * as ec from 'react-native-ecc' 15 | import { Buffer } from 'buffer' 16 | 17 | // if you want to be able to find your keys 18 | // next time, make sure to use the same service ID 19 | ec.setServiceID('be.excellent.to.each.other') 20 | // optional 21 | // ec.setAccessGroup('dsadjsakd.com.app.awesome.my') 22 | 23 | // this library allows you to sign 32 byte hashes (e.g. sha256 hashes) 24 | const msg = new Buffer('hey ho') 25 | // check ec.curves for supported curves 26 | const curve = 'p256' 27 | ec.keyPair(curve, function (err, key) { 28 | // pub tested for compatibility with npm library "elliptic" 29 | const pub = key.pub 30 | console.log('pub', key.pub.toString('hex')) 31 | 32 | // look up the key later like this: 33 | // const key = ec.keyFromPublic(pub) 34 | 35 | key.sign({ 36 | data: msg, 37 | algorithm: 'sha256' 38 | }, function (err, sig) { 39 | // signatures tested for compatibility with npm library "elliptic" 40 | console.log('sig', sig.toString('hex')) 41 | key.verify({ 42 | algorithm: 'sha256', 43 | data: msg, 44 | sig: sig 45 | }, function (err, verified) { 46 | console.log('verified:', verified) 47 | }) 48 | }) 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.2.3' 8 | } 9 | } 10 | 11 | apply plugin: 'com.android.library' 12 | 13 | android { 14 | compileSdkVersion 23 15 | buildToolsVersion "23.0.1" 16 | 17 | defaultConfig { 18 | minSdkVersion 19 19 | targetSdkVersion 22 20 | versionCode 1 21 | versionName "1.0" 22 | } 23 | lintOptions { 24 | abortOnError false 25 | warning 'InvalidPackage' 26 | } 27 | } 28 | 29 | repositories { 30 | mavenCentral() 31 | } 32 | 33 | dependencies { 34 | compile "com.facebook.react:react-native:0.19.+" 35 | } 36 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/java/com/rn/ecc/ECCModule.java: -------------------------------------------------------------------------------- 1 | package com.rn.ecc; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.SharedPreferences; 6 | import android.os.Build; 7 | import android.security.KeyPairGeneratorSpec; 8 | import android.security.keystore.KeyGenParameterSpec; 9 | import android.security.keystore.KeyProperties; 10 | import android.util.Base64; 11 | import android.util.Log; 12 | 13 | import com.facebook.react.bridge.Callback; 14 | import com.facebook.react.bridge.ReactApplicationContext; 15 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 16 | import com.facebook.react.bridge.ReactMethod; 17 | import com.facebook.react.bridge.ReadableMap; 18 | 19 | import java.io.IOException; 20 | import java.math.BigInteger; 21 | import java.security.GeneralSecurityException; 22 | import java.security.InvalidAlgorithmParameterException; 23 | import java.security.InvalidKeyException; 24 | import java.security.KeyFactory; 25 | import java.security.KeyPair; 26 | import java.security.KeyPairGenerator; 27 | import java.security.KeyStore; 28 | import java.security.NoSuchAlgorithmException; 29 | import java.security.NoSuchProviderException; 30 | import java.security.PrivateKey; 31 | import java.security.Signature; 32 | import java.security.interfaces.ECPublicKey; 33 | import java.security.spec.ECGenParameterSpec; 34 | import java.security.spec.ECPoint; 35 | import java.security.spec.InvalidKeySpecException; 36 | import java.security.spec.X509EncodedKeySpec; 37 | import java.util.Arrays; 38 | import java.util.Calendar; 39 | import java.util.GregorianCalendar; 40 | import java.util.HashMap; 41 | import java.util.Map; 42 | import java.util.UUID; 43 | 44 | import javax.security.auth.x500.X500Principal; 45 | 46 | /** 47 | * Created by Jacob Gins on 6/2/2016. 48 | */ 49 | public class ECCModule extends ReactContextBaseJavaModule { 50 | private static final String KEYSTORE_PROVIDER_ANDROID_KEYSTORE = "AndroidKeyStore"; 51 | private static final String KEY_TO_ALIAS_MAPPER = "key.to.alias.mapper"; 52 | private static final Map sizeToName = new HashMap(); 53 | private static final Map sizeToHead = new HashMap(); 54 | private SharedPreferences pref; 55 | private final boolean isModern = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M; 56 | 57 | static { 58 | sizeToName.put(192, "secp192r1"); 59 | sizeToName.put(224, "secp224r1"); 60 | sizeToName.put(256, "secp256r1"); 61 | sizeToName.put(384, "secp384r1"); 62 | // sizeToName.put(521, "secp521r1"); 63 | } 64 | 65 | public ECCModule(ReactApplicationContext reactContext) { 66 | super(reactContext); 67 | pref = reactContext.getSharedPreferences(KEY_TO_ALIAS_MAPPER, Context.MODE_PRIVATE); 68 | } 69 | 70 | @Override 71 | public String getName() { 72 | return "RNECC"; 73 | } 74 | 75 | private KeyPairGenerator getKeyPairGenerator(int sizeInBits, String keyAlias) throws 76 | NoSuchAlgorithmException, 77 | InvalidAlgorithmParameterException, 78 | NoSuchProviderException { 79 | return isModern ? getNewKeyPairGenerator(sizeInBits, keyAlias) : getOldKeyPairGenerator(sizeInBits, keyAlias); 80 | } 81 | 82 | @TargetApi(Build.VERSION_CODES.M) 83 | private KeyPairGenerator getNewKeyPairGenerator(int sizeInBits, String keyAlias) throws 84 | NoSuchAlgorithmException, 85 | InvalidAlgorithmParameterException, 86 | NoSuchProviderException { 87 | KeyPairGenerator kpg = KeyPairGenerator.getInstance( 88 | KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); 89 | kpg.initialize(new KeyGenParameterSpec.Builder( 90 | keyAlias, 91 | KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY) 92 | .setDigests(KeyProperties.DIGEST_SHA256, 93 | KeyProperties.DIGEST_SHA512, 94 | KeyProperties.DIGEST_NONE) 95 | .setKeySize(sizeInBits) 96 | .build()); 97 | 98 | return kpg; 99 | } 100 | 101 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 102 | private KeyPairGenerator getOldKeyPairGenerator(int sizeInBits, String keyAlias) throws 103 | NoSuchAlgorithmException, 104 | InvalidAlgorithmParameterException, 105 | NoSuchProviderException { 106 | Calendar start = new GregorianCalendar(); 107 | Calendar end = new GregorianCalendar(); 108 | end.add(Calendar.YEAR, 10); 109 | KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, KEYSTORE_PROVIDER_ANDROID_KEYSTORE); 110 | Context context = getReactApplicationContext().getApplicationContext(); 111 | kpg.initialize(new KeyPairGeneratorSpec.Builder(context) 112 | .setAlias(keyAlias) 113 | // KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY) 114 | // .setDigests(KeyProperties.DIGEST_SHA256, 115 | // KeyProperties.DIGEST_SHA512, 116 | // KeyProperties.DIGEST_NONE) 117 | .setKeyType(KeyProperties.KEY_ALGORITHM_EC) 118 | .setKeySize(sizeInBits) 119 | .setStartDate(start.getTime()) 120 | .setEndDate(end.getTime()) 121 | .setSubject(new X500Principal("CN=" + keyAlias)) 122 | .setSerialNumber(BigInteger.valueOf(Math.abs(keyAlias.hashCode()))) 123 | .build()); 124 | return kpg; 125 | } 126 | 127 | @ReactMethod 128 | public void generateECPair(ReadableMap map, Callback function) { 129 | int sizeInBits = map.getInt("bits"); 130 | String keyAlias = UUID.randomUUID().toString(); 131 | try { 132 | KeyStore ks = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID_KEYSTORE); 133 | ks.load(null); 134 | 135 | /* 136 | * Generate a new EC key pair entry in the Android Keystore by 137 | * using the KeyPairGenerator API. The private key can only be 138 | * used for signing or verification and only with SHA-256, 139 | * SHA-512 or NONE as the message digest. 140 | */ 141 | KeyPairGenerator kpg = getKeyPairGenerator(sizeInBits, keyAlias); 142 | KeyPair kp = kpg.genKeyPair(); 143 | ECPublicKey publicKey = (ECPublicKey)kp.getPublic(); 144 | byte[] publicKeyBytes = encodeECPublicKey(publicKey); 145 | String publicKeyString = toBase64(publicKeyBytes); 146 | SharedPreferences.Editor editor = pref.edit(); 147 | editor.putString(publicKeyString, keyAlias); 148 | editor.commit(); 149 | 150 | function.invoke(null, publicKeyString); 151 | 152 | } catch (Exception ex) { 153 | Log.e("generateECPair", "ERR", ex); 154 | function.invoke(ex.toString(), null); 155 | } 156 | } 157 | 158 | @ReactMethod 159 | public void hasKey(String publicKeyString, Callback function) { 160 | try { 161 | KeyStore.Entry entry = getEntry(publicKeyString); 162 | function.invoke(null, entry instanceof KeyStore.PrivateKeyEntry); 163 | } catch (Exception ex) { 164 | Log.e("hasKey", "ERR", ex); 165 | function.invoke(ex.toString(), null); 166 | return; 167 | } 168 | } 169 | 170 | public KeyStore.Entry getEntry (String publicKeyString) throws GeneralSecurityException, IOException { 171 | String keyAlias = pref.getString(publicKeyString, null); 172 | if (keyAlias == null) { 173 | return null; 174 | } 175 | 176 | KeyStore ks = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID_KEYSTORE); 177 | ks.load(null); 178 | return ks.getEntry(keyAlias, null); 179 | } 180 | 181 | @Override 182 | public Map getConstants() { 183 | final Map constants = new HashMap<>(); 184 | constants.put("preHash", this.isModern); 185 | return constants; 186 | } 187 | 188 | @ReactMethod 189 | public void sign(ReadableMap map, Callback function) { 190 | String publicKeyString = map.getString("pub"); 191 | String algorithm = getAlgorithm(map); 192 | String signature = ""; 193 | try { 194 | byte[] data = getDataProp(map); 195 | KeyStore.Entry entry = getEntry(publicKeyString); 196 | Signature s = Signature.getInstance(algorithm); 197 | PrivateKey key = ((KeyStore.PrivateKeyEntry) entry).getPrivateKey(); 198 | s.initSign(key); 199 | s.update(data); 200 | byte[] signatureBytes = s.sign(); 201 | signature = toBase64(signatureBytes); 202 | // WritableMap toVerify = new WritableNativeMap(); 203 | // toVerify.putString("sig", signature); 204 | // toVerify.putString("pub", publicKeyString); 205 | // toVerify.putString("data", map.getString("data")); 206 | // toVerify.putString("algorithm", map.getString("algorithm")); 207 | // boolean good = verify(toVerify); 208 | // if (!good) { 209 | // Log.e("sign", "ERR"); 210 | // } 211 | } catch (Exception ex) { 212 | Log.e("sign", "ERR", ex); 213 | function.invoke(ex.toString(), null); 214 | return; 215 | } 216 | 217 | function.invoke(null, signature); 218 | } 219 | 220 | @ReactMethod 221 | public void verify(ReadableMap map, Callback function) { 222 | try { 223 | boolean verified = verify(map); 224 | function.invoke(null, verified); 225 | } catch (Exception ex) { 226 | Log.e("RNECC", "verify error", ex); 227 | function.invoke(ex.toString(), null); 228 | return; 229 | } 230 | } 231 | 232 | private boolean verify (ReadableMap map) throws GeneralSecurityException { 233 | byte[] data = getDataProp(map); 234 | byte[] sigBytes = getSigProp(map); 235 | byte[] pubKeyBytes = getPubProp(map); 236 | String algorithm = getAlgorithm(map); 237 | ECPublicKey publicKey = decodeECPublicKey(pubKeyBytes); 238 | Signature sig = Signature.getInstance(algorithm); 239 | sig.initVerify(publicKey); 240 | sig.update(data); 241 | return sig.verify(sigBytes); 242 | } 243 | 244 | private static byte[] encodeECPublicKey(ECPublicKey pubKey) throws InvalidKeyException { 245 | int keyLengthBytes = pubKey.getParams().getOrder().bitLength() / 8; 246 | 247 | ECPoint w = pubKey.getW(); 248 | BigInteger x = w.getAffineX(); 249 | BigInteger y = w.getAffineY(); 250 | byte[] b = combine(x, y, keyLengthBytes * 2); 251 | byte[] publicKeyEncoded = new byte[1 + 2 * keyLengthBytes]; 252 | publicKeyEncoded[0] = 0x04; 253 | for (int i = 0; i < b.length; i++) { 254 | publicKeyEncoded[i + 1] = b[i]; 255 | } 256 | 257 | return publicKeyEncoded; 258 | } 259 | 260 | private static String toBase64(byte[] bytes) { 261 | return Base64.encodeToString(bytes, Base64.NO_WRAP); 262 | } 263 | 264 | private static byte[] fromBase64(String str) { 265 | return Base64.decode(str, Base64.NO_WRAP); 266 | } 267 | 268 | // courtesy of: 269 | // http://stackoverflow.com/questions/30445997/loading-raw-64-byte-long-ecdsa-public-key-in-java 270 | private static byte[] createHeadForNamedCurve(int size) 271 | throws NoSuchAlgorithmException, 272 | InvalidAlgorithmParameterException { 273 | KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); 274 | String name = sizeToName.get(size); 275 | ECGenParameterSpec m = new ECGenParameterSpec(name); 276 | kpg.initialize(m); 277 | KeyPair kp = kpg.generateKeyPair(); 278 | byte[] encoded = kp.getPublic().getEncoded(); 279 | return Arrays.copyOf(encoded, encoded.length - 2 * (size / Byte.SIZE)); 280 | } 281 | 282 | public static ECPublicKey decodeECPublicKey(byte[] pubKeyBytes) 283 | throws InvalidKeySpecException, 284 | InvalidKeyException, 285 | NoSuchAlgorithmException, 286 | InvalidAlgorithmParameterException { 287 | // uncompressed keys only 288 | if (pubKeyBytes[0] != 0x04) { 289 | throw new InvalidKeyException("only uncompressed keys supported"); 290 | } 291 | 292 | byte[] w = Arrays.copyOfRange(pubKeyBytes, 1, pubKeyBytes.length); 293 | int size = w.length / 2 * Byte.SIZE; 294 | byte[] head = sizeToHead.get(size); 295 | if (head == null) { 296 | head = createHeadForNamedCurve(size); 297 | sizeToHead.put(size, head); 298 | } 299 | 300 | byte[] encodedKey = new byte[head.length + w.length]; 301 | System.arraycopy(head, 0, encodedKey, 0, head.length); 302 | System.arraycopy(w, 0, encodedKey, head.length, w.length); 303 | KeyFactory eckf; 304 | try { 305 | eckf = KeyFactory.getInstance("EC"); 306 | } catch (NoSuchAlgorithmException e) { 307 | throw new IllegalStateException("EC key factory not present in runtime"); 308 | } 309 | 310 | X509EncodedKeySpec ecpks = new X509EncodedKeySpec(encodedKey); 311 | try { 312 | return (ECPublicKey) eckf.generatePublic(ecpks); 313 | } catch (Exception e) { 314 | Log.e("RNECC", "failed to decode EC pubKey", e); 315 | throw e; 316 | } 317 | } 318 | 319 | // https://github.com/i2p/i2p.i2p/blob/master/core/java/src/net/i2p/crypto/SigUtil.java 320 | 321 | /** 322 | * @param bi non-negative 323 | * @return array of exactly len bytes 324 | */ 325 | public static byte[] rectify(BigInteger bi, int len) 326 | throws InvalidKeyException { 327 | byte[] b = bi.toByteArray(); 328 | if (b.length == len) { 329 | // just right 330 | return b; 331 | } 332 | if (b.length > len + 1) 333 | throw new InvalidKeyException("key too big (" + b.length + ") max is " + (len + 1)); 334 | byte[] rv = new byte[len]; 335 | if (b.length == 0) 336 | return rv; 337 | if ((b[0] & 0x80) != 0) 338 | throw new InvalidKeyException("negative"); 339 | if (b.length > len) { 340 | // leading 0 byte 341 | if (b[0] != 0) 342 | throw new InvalidKeyException("key too big (" + b.length + ") max is " + len); 343 | System.arraycopy(b, 1, rv, 0, len); 344 | } else { 345 | // smaller 346 | System.arraycopy(b, 0, rv, len - b.length, b.length); 347 | } 348 | return rv; 349 | } 350 | 351 | /** 352 | * Combine two BigIntegers of nominal length = len / 2 353 | * @return array of exactly len bytes 354 | */ 355 | private static byte[] combine(BigInteger x, BigInteger y, int len) 356 | throws InvalidKeyException { 357 | int sublen = len / 2; 358 | byte[] b = new byte[len]; 359 | byte[] bx = rectify(x, sublen); 360 | byte[] by = rectify(y, sublen); 361 | System.arraycopy(bx, 0, b, 0, sublen); 362 | System.arraycopy(by, 0, b, sublen, sublen); 363 | return b; 364 | } 365 | 366 | private static byte[] getDataProp(ReadableMap map) { 367 | if (map.hasKey("data")) { 368 | return getBinaryProp(map, "data"); 369 | } 370 | 371 | return getBinaryProp(map, "hash"); 372 | } 373 | 374 | private static byte[] getSigProp (ReadableMap map) { 375 | return getBinaryProp(map, "sig"); 376 | } 377 | 378 | private static byte[] getPubProp (ReadableMap map) { 379 | return getBinaryProp(map, "pub"); 380 | } 381 | 382 | private static byte[] getBinaryProp (ReadableMap map, String propName) { 383 | return fromBase64(map.getString(propName)); 384 | } 385 | 386 | private static String getAlgorithm (ReadableMap map) { 387 | String alg = map.hasKey("algorithm") ? map.getString("algorithm") : "NONE"; 388 | return alg.toUpperCase() + "withECDSA"; 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /android/src/main/java/com/rn/ecc/ECCPackage.java: -------------------------------------------------------------------------------- 1 | package com.rn.ecc; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.JavaScriptModule; 5 | import com.facebook.react.bridge.NativeModule; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.uimanager.ViewManager; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | public class ECCPackage implements ReactPackage { 14 | 15 | @Override 16 | public List createNativeModules(ReactApplicationContext reactContext) { 17 | List modules = new ArrayList<>(); 18 | 19 | modules.add(new ECCModule(reactContext)); 20 | 21 | return modules; 22 | } 23 | 24 | @Override 25 | public List createViewManagers(ReactApplicationContext reactContext) { 26 | return Collections.emptyList(); 27 | } 28 | 29 | public List> createJSModules() { 30 | return Collections.emptyList(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { NativeModules, Platform } from 'react-native' 4 | import { Buffer } from 'buffer' 5 | import hasher from 'hash.js' 6 | const { RNECC } = NativeModules 7 | const preHash = RNECC.preHash !== false 8 | const isAndroid = Platform.OS === 'android' 9 | const encoding = 'base64' 10 | const curves = { 11 | p192: 192, 12 | p224: 224, 13 | p256: 256, 14 | p384: 384, 15 | secp192r1: 192, 16 | secp256r1: 256, 17 | secp224r1: 224, 18 | secp384r1: 384 19 | // p521: 521 // should be supported, but SecKeyRawSign fails with OSStatus -1 20 | } 21 | 22 | let serviceID 23 | let accessGroup 24 | 25 | module.exports = { 26 | encoding, 27 | curves, 28 | setServiceID, 29 | getServiceID, 30 | setAccessGroup, 31 | getAccessGroup, 32 | keyPair, 33 | sign, 34 | verify, 35 | lookupKey, 36 | hasKey, 37 | keyFromPublic 38 | } 39 | 40 | function setServiceID (id) { 41 | if (serviceID) throw new Error('serviceID can only be set once') 42 | 43 | serviceID = id 44 | } 45 | 46 | function getServiceID () { 47 | return serviceID 48 | } 49 | 50 | function setAccessGroup (val) { 51 | if (accessGroup) throw new Error('accessGroup can only be set once') 52 | 53 | accessGroup = val 54 | } 55 | 56 | function getAccessGroup () { 57 | return accessGroup 58 | } 59 | 60 | /** 61 | * generates a new key pair, calls back with pub key 62 | * @param {String} curve - elliptic curve 63 | * @param {Function} cb - calls back with a new key with the API: { sign, verify, pub } 64 | */ 65 | function keyPair (curve, cb) { 66 | checkServiceID() 67 | assert(typeof curve === 'string') 68 | assert(typeof cb === 'function') 69 | if (!(curve in curves)) throw new Error('unsupported curve') 70 | 71 | let sizeInBits = curves[curve] 72 | RNECC.generateECPair({ 73 | curve: curve, 74 | bits: sizeInBits, 75 | service: serviceID, 76 | accessGroup: accessGroup 77 | }, function (err, base64pubKey) { 78 | cb(convertError(err), base64pubKey && keyFromPublic(toBuffer(base64pubKey))) 79 | }) 80 | } 81 | 82 | /** 83 | * signs a hash 84 | * @param {Buffer|String} options.pubKey - pubKey corresponding to private key to sign hash with 85 | * @param {Buffer|String} options.data - data to sign 86 | * @param {String} options.algorithm - algorithm to use to hash data before signing 87 | * @param {Function} cb 88 | */ 89 | function sign ({ pubKey, data, algorithm }, cb) { 90 | checkServiceID() 91 | assert(Buffer.isBuffer(pubKey) || typeof pubKey === 'string') 92 | assert(Buffer.isBuffer(data) || typeof data === 'string') 93 | 94 | checkNotCompact(pubKey) 95 | 96 | const opts = { 97 | service: serviceID, 98 | accessGroup: accessGroup, 99 | pub: pubKey 100 | } 101 | 102 | assert(typeof cb === 'function') 103 | if (preHash) { 104 | opts.hash = getHash(data, algorithm) 105 | } else { 106 | opts.data = data 107 | opts.algorithm = algorithm 108 | } 109 | 110 | RNECC.sign(normalizeOpts(opts), normalizeCallback(cb)) 111 | } 112 | 113 | /** 114 | * verifies a signature 115 | * @param {Buffer|String} options.pubKey - pubKey corresponding to private key to sign hash with 116 | * @param {Buffer|String} options.data - signed data 117 | * @param {String} options.algorithm - algorithm used to hash data before it was signed 118 | * @param {Buffer} options.sig - signature 119 | * @param {Function} cb 120 | */ 121 | function verify ({ pubKey, data, algorithm, sig }, cb) { 122 | checkNotCompact(pubKey) 123 | 124 | assert(Buffer.isBuffer(data) || typeof data === 'string') 125 | assert(typeof pubKey === 'string' || Buffer.isBuffer(pubKey)) 126 | assert(typeof cb === 'function') 127 | 128 | const opts = { 129 | pub: pubKey, 130 | sig, 131 | } 132 | 133 | if (preHash) { 134 | opts.hash = getHash(data, algorithm) 135 | } else { 136 | opts.data = data 137 | opts.algorithm = algorithm 138 | } 139 | 140 | RNECC.verify(normalizeOpts(opts), normalizeCallback(cb)) 141 | } 142 | 143 | function normalizeOpts (opts) { 144 | ;['data', 'hash', 'pub', 'sig'].forEach(prop => { 145 | if (opts[prop]) opts[prop] = toString(opts[prop]) 146 | }) 147 | 148 | return opts 149 | } 150 | 151 | function hasKey (pubKey, cb) { 152 | checkServiceID() 153 | assert(Buffer.isBuffer(pubKey) || typeof pubKey === 'string') 154 | checkNotCompact(pubKey) 155 | pubKey = toString(pubKey) 156 | cb = normalizeCallback(cb) 157 | if (isAndroid) return RNECC.hasKey(pubKey, cb) 158 | 159 | RNECC.hasKey({ 160 | service: serviceID, 161 | accessGroup: accessGroup, 162 | pub: pubKey 163 | }, cb) 164 | } 165 | 166 | function lookupKey (pubKey, cb) { 167 | hasKey(pubKey, function (err, exists) { 168 | if (err) return cb(convertError(err)) 169 | if (exists) return cb(null, keyFromPublic(pubKey)) 170 | 171 | cb(new Error('NotFound')) 172 | }) 173 | } 174 | 175 | /** 176 | * Returns a key with the API as the one returned by keyPair(...) 177 | * @param {Buffer} pub pubKey buffer for existing key (created with keyPair(...)) 178 | * @return {Object} key 179 | */ 180 | function keyFromPublic (pubKey) { 181 | checkNotCompact(pubKey) 182 | let base64pub = toString(pubKey) 183 | return { 184 | sign: (opts, cb) => { 185 | sign({ ...opts, pubKey: base64pub }, cb) 186 | }, 187 | verify: (opts, cb) => { 188 | verify({ ...opts, pubKey: base64pub }, cb) 189 | }, 190 | pub: pubKey 191 | } 192 | } 193 | 194 | function assert (statement, errMsg) { 195 | if (!statement) throw new Error(errMsg || 'assertion failed') 196 | } 197 | 198 | function toString (buf) { 199 | if (typeof buf === 'string') return buf 200 | if (Buffer.isBuffer(buf)) return buf.toString(encoding) 201 | 202 | return buf.toString() 203 | } 204 | 205 | function toBuffer (str) { 206 | if (Buffer.isBuffer(str)) return str 207 | if (typeof str === 'string') return new Buffer(str, encoding) 208 | 209 | throw new Error('expected string or buffer') 210 | } 211 | 212 | function checkServiceID () { 213 | if (!serviceID) { 214 | throw new Error('call setServiceID() first') 215 | } 216 | } 217 | 218 | function convertError (error) { 219 | if (!error) { 220 | return null; 221 | } 222 | 223 | var message = error.message || (typeof error === 'string' ? error : JSON.stringify(error)) 224 | var out = new Error(message) 225 | out.key = error.key // flow doesn't like this :( 226 | return out 227 | } 228 | 229 | function normalizeCallback (cb) { 230 | return function (err, result) { 231 | if (err) return cb(convertError(err)) 232 | 233 | result = typeof result === 'string' 234 | ? toBuffer(result) 235 | : result 236 | 237 | return cb(null, result) 238 | } 239 | } 240 | 241 | function getHash (data, algorithm) { 242 | if (!algorithm) return data 243 | 244 | const arr = hasher[algorithm]().update(data).digest() 245 | return new Buffer(arr) 246 | } 247 | 248 | function checkNotCompact (pub) { 249 | assert(toBuffer(pub)[0] === 4, 'compact keys not supported') 250 | } 251 | -------------------------------------------------------------------------------- /ios/RNECC.h: -------------------------------------------------------------------------------- 1 | // 2 | // RNECC.h 3 | // 4 | // Created by Mark Vayngrib on 12/24/15. 5 | // Copyright © 2015 Tradle, Inc. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | @interface RNECC : NSObject 12 | 13 | - (NSString *) toPublicIdentifier:(NSString *)privIdentifier; 14 | - (NSData *) getPublicKeyDataByLabel:(NSString *)label; 15 | - (SecKeyRef) getPublicKeyRef:(NSString *)base64pub; 16 | - (SecKeyRef) getPrivateKeyRef:(NSString *)serviceID pub:(NSString *)base64pub status:(OSStatus *)status; 17 | - (OSStatus) tagKeyWithLabel:(NSString*)label tag:(NSString*)tag; 18 | - (NSString *) uuidString; 19 | - (NSData *)sign:(nonnull NSDictionary*)options errMsg:(NSString **) errMsg; 20 | - (BOOL) verify:(NSString *)base64pub hash:(NSData *)hash sig:(NSData *)sig errMsg:(NSString **)errMsg; 21 | @end 22 | -------------------------------------------------------------------------------- /ios/RNECC.m: -------------------------------------------------------------------------------- 1 | // 2 | // RNECC.m 3 | // 4 | // Created by Mark Vayngrib on 12/24/15. 5 | // Copyright © 2015 Tradle, Inc. All rights reserved. 6 | // 7 | 8 | #import "RNECC.h" 9 | #include "CommonCrypto/CommonDigest.h" 10 | #import 11 | 12 | #define HASH_LENGTH CC_SHA256_DIGEST_LENGTH 13 | #define kTypeOfSigPadding kSecPaddingPKCS1 14 | 15 | #if TARGET_OS_SIMULATOR 16 | static BOOL isSimulator = YES; 17 | #else 18 | static BOOL isSimulator = NO; 19 | #endif 20 | 21 | @implementation RNECC 22 | 23 | RCT_EXPORT_MODULE(); 24 | 25 | RCT_EXPORT_METHOD(test) 26 | { 27 | NSString* errMsg; 28 | NSString* serviceId = @"this.is.a.test"; 29 | NSString* pub = [self generateECPair:@{ 30 | @"service": serviceId, 31 | @"bits": @256 32 | } 33 | errMsg:&errMsg]; 34 | 35 | if (pub == nil) return; 36 | 37 | NSMutableData* hash = [NSMutableData dataWithLength:HASH_LENGTH]; 38 | SecRandomCopyBytes(kSecRandomDefault, HASH_LENGTH, [hash mutableBytes]); 39 | NSDictionary* options = @{ 40 | @"service": serviceId, 41 | @"pub":pub, 42 | @"hash":[hash base64EncodedStringWithOptions:0] 43 | }; 44 | 45 | NSData* sig = [self sign:options errMsg:&errMsg]; 46 | if (sig == nil) return; 47 | 48 | BOOL verified = [self verify:pub hash:hash sig:sig errMsg:&errMsg]; 49 | NSLog(@"success: %i", verified); 50 | } 51 | 52 | RCT_EXPORT_METHOD(generateECPair:(nonnull NSDictionary*) options 53 | callback:(RCTResponseSenderBlock)callback) { 54 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 55 | NSString* errMsg; 56 | NSString* base64pub = [self generateECPair:options errMsg:&errMsg]; 57 | if (base64pub == nil) { 58 | return callback(@[rneccMakeError(errMsg)]); 59 | } else { 60 | callback(@[[NSNull null], base64pub]); 61 | } 62 | }); 63 | } 64 | 65 | /** 66 | * @return base64 pub key string 67 | */ 68 | - (NSString *) generateECPair:(nonnull NSDictionary*) options 69 | errMsg:(NSString **)errMsg 70 | { 71 | CFErrorRef sacErr = NULL; 72 | SecAccessControlRef sacObject; 73 | 74 | // Should be the secret invalidated when passcode is removed? If not then use `kSecAttrAccessibleWhenUnlocked`. 75 | sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault, 76 | kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, 77 | // kSecAccessControlTouchIDAny | kSecAccessControlPrivateKeyUsage, 78 | // kSecAccessControlUserPresence, 79 | kNilOptions, 80 | &sacErr); 81 | 82 | if (sacErr) { 83 | *errMsg = [(__bridge NSError *)sacErr description]; 84 | return nil; 85 | } 86 | 87 | // Create parameters dictionary for key generation. 88 | NSString* uuid = [self uuidString]; 89 | NSString* pubKeyLabel = [self toPublicIdentifier:uuid]; 90 | NSMutableDictionary *privateKeyAttrs = [NSMutableDictionary dictionaryWithDictionary: @{ 91 | (__bridge id)kSecAttrIsPermanent: @YES, 92 | (__bridge id)kSecAttrApplicationLabel: uuid, 93 | }]; 94 | 95 | if (!isSimulator) { 96 | [privateKeyAttrs setObject:(__bridge_transfer id)sacObject forKey:(__bridge id)kSecAttrAccessControl]; 97 | // [privateKeyAttrs setObject:(__bridge id)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible]; 98 | } 99 | 100 | NSString* serviceID = [options valueForKey:@"service"]; 101 | NSNumber* sizeInBits = [options objectForKey:@"bits"]; 102 | if (sizeInBits == nil) { 103 | sizeInBits = @256; 104 | } 105 | 106 | NSString* accessGroup = [options valueForKey:@"accessGroup"]; 107 | if (accessGroup) { 108 | [privateKeyAttrs setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup]; 109 | } 110 | 111 | NSDictionary *publicKeyAttrs = @{ 112 | (__bridge id)kSecAttrIsPermanent: isSimulator ? @YES : @NO, 113 | (__bridge id)kSecAttrApplicationLabel: pubKeyLabel, 114 | }; 115 | 116 | NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary: @{ 117 | (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeEC, 118 | (__bridge id)kSecAttrKeySizeInBits: sizeInBits, 119 | (__bridge id)kSecPrivateKeyAttrs: privateKeyAttrs, 120 | (__bridge id)kSecPublicKeyAttrs: publicKeyAttrs, 121 | }]; 122 | 123 | if (accessGroup) { 124 | [parameters setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup]; 125 | } 126 | 127 | if (sizeInBits == @256 && !isSimulator && floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_8_0) { 128 | NSOperatingSystemVersion os = [[NSProcessInfo processInfo] operatingSystemVersion]; 129 | if (os.majorVersion >= 9) { 130 | [parameters setObject:(__bridge id)kSecAttrTokenIDSecureEnclave forKey:(__bridge id)kSecAttrTokenID]; 131 | } 132 | } 133 | 134 | SecKeyRef publicKey, privateKey; 135 | OSStatus status = SecKeyGeneratePair((__bridge CFDictionaryRef)parameters, &publicKey, &privateKey); 136 | if (status != errSecSuccess) { 137 | *errMsg = keychainStatusToString(status); 138 | return nil; 139 | } 140 | 141 | if (!isSimulator) { 142 | status = SecItemAdd((__bridge CFDictionaryRef)@{ 143 | (__bridge id)kSecClass: (__bridge id)kSecClassKey, 144 | (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPublic, 145 | (__bridge id)kSecAttrApplicationLabel: pubKeyLabel, 146 | (__bridge id)kSecValueRef: (__bridge id)publicKey 147 | }, nil); 148 | 149 | if (status != errSecSuccess) { 150 | CFRelease(privateKey); 151 | CFRelease(publicKey); 152 | *errMsg = keychainStatusToString(status); 153 | return nil; 154 | } 155 | } 156 | 157 | NSData *data = [self getPublicKeyDataByLabel:pubKeyLabel]; 158 | NSString* base64str = [data base64EncodedStringWithOptions:0]; 159 | 160 | sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault, 161 | kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, 162 | 0, &sacErr); 163 | 164 | status = SecItemAdd((__bridge CFDictionaryRef)@{ 165 | (__bridge id)kSecAttrAccessControl: (__bridge_transfer id)sacObject, 166 | (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, 167 | (__bridge id)kSecAttrService: serviceID, 168 | (__bridge id)kSecAttrAccount:base64str, 169 | (__bridge id)kSecAttrGeneric:uuid, 170 | }, nil); 171 | 172 | if (status != errSecSuccess) { 173 | CFRelease(privateKey); 174 | CFRelease(publicKey); 175 | *errMsg = keychainStatusToString(status); 176 | return nil; 177 | } 178 | 179 | 180 | status = [self tagKeyWithLabel:pubKeyLabel tag:[self toPublicIdentifier:base64str]]; 181 | 182 | CFRelease(privateKey); 183 | CFRelease(publicKey); 184 | if (status != errSecSuccess) { 185 | *errMsg = keychainStatusToString(status); 186 | return nil; 187 | } 188 | 189 | return base64str; 190 | } 191 | 192 | RCT_EXPORT_METHOD(hasKey:(nonnull NSDictionary *)options 193 | callback:(RCTResponseSenderBlock)callback) 194 | { 195 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 196 | OSStatus status; 197 | SecKeyRef privateKey = [self getPrivateKeyRef:options status:&status]; 198 | if (privateKey) { 199 | CFRelease(privateKey); 200 | callback(@[[NSNull null], @YES]); 201 | } else if (status == errSecItemNotFound) { 202 | callback(@[[NSNull null], @NO]); 203 | } else { 204 | callback(@[rneccMakeError(keychainStatusToString(status))]); 205 | } 206 | }); 207 | } 208 | 209 | RCT_EXPORT_METHOD(sign:(nonnull NSDictionary *)options 210 | // withAuthenticationPrompt:(NSString *)prompt 211 | callback:(RCTResponseSenderBlock)callback) { 212 | // Query private key object from the keychain. 213 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 214 | NSString* errMsg; 215 | NSData* sig = [self sign:options errMsg:&errMsg]; 216 | if (!sig) { 217 | callback(@[rneccMakeError(errMsg)]); 218 | return; 219 | } 220 | 221 | NSString* base64sig = [sig base64EncodedStringWithOptions:0]; 222 | callback(@[[NSNull null], base64sig]); 223 | }); 224 | } 225 | 226 | -(NSData *)sign:(nonnull NSDictionary*)options 227 | errMsg:(NSString **) errMsg { 228 | 229 | OSStatus status; 230 | NSString* base64hash = [options valueForKey:@"hash"]; 231 | NSData *hash = [[NSData alloc] initWithBase64EncodedString:base64hash options:0]; 232 | if ([hash length] != HASH_LENGTH) { 233 | *errMsg = [NSString stringWithFormat:@"hash parameter must be %d bytes", HASH_LENGTH]; 234 | return nil; 235 | } 236 | 237 | SecKeyRef privateKey = [self getPrivateKeyRef:options status:&status]; 238 | if (!privateKey) { 239 | *errMsg = keychainStatusToString(status); 240 | return nil; 241 | } 242 | 243 | // Sign the data in the digest/digestLength memory block. 244 | uint8_t signature[128]; 245 | size_t signatureLength = sizeof(signature); 246 | status = SecKeyRawSign( 247 | privateKey, 248 | kTypeOfSigPadding, 249 | (const uint8_t*)[hash bytes], 250 | HASH_LENGTH, 251 | signature, 252 | &signatureLength); 253 | 254 | CFRelease(privateKey); 255 | if (status != errSecSuccess) { 256 | *errMsg = keychainStatusToString(status); 257 | return nil; 258 | } 259 | 260 | // NSError* vError; 261 | NSData* sigData = [NSData dataWithBytes:(const void *)signature length:signatureLength]; 262 | // BOOL verified = [self verify:base64pub hash:hash sig:sigData error:&vError]; 263 | // if (!verified) { 264 | // NSLog(@"uh oh, failed to verify sig"); 265 | // } 266 | 267 | return sigData; 268 | } 269 | 270 | RCT_EXPORT_METHOD(verify:(nonnull NSDictionary *)options 271 | callback:(RCTResponseSenderBlock)callback) { 272 | NSString* pub = [options valueForKey:@"pub"]; 273 | NSString* hash = [options valueForKey:@"hash"]; 274 | NSString* sig = [options valueForKey:@"sig"]; 275 | [self doVerify:pub hash:hash sig:sig callback:callback]; 276 | } 277 | 278 | -(OSStatus) importPubKey:(NSString *)base64pub { 279 | 280 | NSData *keyData = [[NSData alloc] initWithBase64EncodedString:base64pub options:0]; 281 | // one byte prefix, then key 282 | // if first byte is 0x04, it's a regular public key, if it's 0x02 or 0x03, then it's compact 283 | // the byteLength is compactLength - 1 or (regularLength - 1) / 2 284 | // NSNumber* sizeInBits = byteLength * 8 285 | NSDictionary *saveDict = @{ 286 | (__bridge id) kSecClass : (__bridge id) kSecClassKey, 287 | (__bridge id) kSecAttrKeyType : (__bridge id) kSecAttrKeyTypeEC, 288 | (__bridge id) kSecAttrApplicationTag : [self toPublicIdentifier:base64pub], 289 | (__bridge id) kSecAttrKeyClass : (__bridge id)kSecAttrKeyClassPublic, 290 | (__bridge id) kSecValueData : keyData, 291 | // (__bridge id) kSecAttrKeySizeInBits : sizeInBits, 292 | // (__bridge id) kSecAttrEffectiveKeySize : sizeInBits, 293 | (__bridge id) kSecAttrCanDerive : (__bridge id) kCFBooleanFalse, 294 | // (__bridge id) kSecAttrCanEncrypt : (__bridge id) kCFBooleanTrue, 295 | // (__bridge id) kSecAttrCanDecrypt : (__bridge id) kCFBooleanFalse, 296 | (__bridge id) kSecAttrCanVerify : (__bridge id) kCFBooleanTrue, 297 | (__bridge id) kSecAttrCanSign : (__bridge id) kCFBooleanFalse, 298 | (__bridge id) kSecAttrCanWrap : (__bridge id) kCFBooleanTrue, 299 | (__bridge id) kSecAttrCanUnwrap : (__bridge id) kCFBooleanFalse 300 | }; 301 | 302 | SecKeyRef savedKeyRef = NULL; 303 | return SecItemAdd((__bridge CFDictionaryRef)saveDict, (CFTypeRef *)&savedKeyRef); 304 | // if (sanityCheck != errSecSuccess) { 305 | // 306 | // } 307 | } 308 | 309 | -(BOOL) verify:(NSString *)base64pub 310 | hash:(NSData *)hash 311 | sig:(NSData *)sig 312 | errMsg:(NSString **)errMsg { 313 | 314 | // we might already have the key in the keychain 315 | SecKeyRef publicKey = [self getPublicKeyRef:base64pub]; 316 | if (!publicKey) { 317 | // import the key, then query for it 318 | OSStatus status = [self importPubKey:base64pub]; 319 | if (status != errSecSuccess && status != errSecDuplicateItem) { 320 | *errMsg = keychainStatusToString(errSecBadReq); 321 | return false; 322 | } 323 | 324 | publicKey = [self getPublicKeyRef:base64pub]; 325 | if (!publicKey) { 326 | *errMsg = keychainStatusToString(errSecItemNotFound); 327 | return false; 328 | } 329 | } 330 | 331 | OSStatus status = SecKeyRawVerify( 332 | publicKey, 333 | kTypeOfSigPadding, 334 | (const uint8_t *)[hash bytes], 335 | HASH_LENGTH, 336 | (const uint8_t *)[sig bytes], 337 | [sig length] 338 | ); 339 | 340 | CFRelease(publicKey); 341 | if (status != errSecSuccess) { 342 | *errMsg = keychainStatusToString(status); 343 | return false; 344 | } 345 | 346 | return true; 347 | } 348 | 349 | -(void) doVerify:(NSString *)base64pub 350 | hash:(NSString *)base64hash 351 | sig:(NSString *)sig 352 | callback:(RCTResponseSenderBlock)callback { 353 | NSData *hash = [[NSData alloc] initWithBase64EncodedString:base64hash options:0]; 354 | if ([hash length] != HASH_LENGTH) { 355 | NSString* message = [NSString stringWithFormat:@"hash parameter must be %d bytes", HASH_LENGTH]; 356 | callback(@[rneccMakeError(message)]); 357 | return; 358 | } 359 | 360 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 361 | NSData* sigData = [[NSData alloc] initWithBase64EncodedString:sig options:0]; 362 | NSString* errMsg = nil; 363 | BOOL verified = [self verify:base64pub hash:hash sig:sigData errMsg:&errMsg]; 364 | if (!verified) { 365 | callback(@[rneccMakeError(errMsg), @NO]); 366 | return; 367 | } 368 | 369 | callback(@[[NSNull null], @YES]); 370 | }); 371 | } 372 | 373 | -(OSStatus) tagKeyWithLabel:(NSString*)label tag:(NSString*)tag 374 | { 375 | SecKeyRef foundItem; 376 | OSStatus findStatus = SecItemCopyMatching((__bridge CFDictionaryRef)@{ 377 | (__bridge id)kSecClass: (__bridge id)kSecClassKey, 378 | (__bridge id)kSecAttrApplicationLabel: label, 379 | (__bridge id)kSecReturnAttributes: @YES, 380 | }, (CFTypeRef *)&foundItem); 381 | 382 | if (findStatus != errSecSuccess) { 383 | NSLog(@"failed to find key: %d", (int)findStatus); 384 | return findStatus; 385 | } 386 | 387 | NSMutableDictionary *updateDict = (__bridge NSMutableDictionary *)foundItem; 388 | [updateDict setObject:tag forKey:(__bridge id)kSecAttrApplicationTag]; 389 | [updateDict removeObjectForKey:(__bridge id)kSecClass]; 390 | OSStatus updateStatus = SecItemUpdate((__bridge CFDictionaryRef)@{ 391 | (__bridge id)kSecClass: (__bridge id)kSecClassKey, 392 | (__bridge id)kSecAttrApplicationLabel: label, 393 | }, (__bridge CFDictionaryRef)updateDict); 394 | 395 | if (updateStatus != errSecSuccess) { 396 | NSLog(@"failed to update key: %d", (int)updateStatus); 397 | return updateStatus; 398 | } 399 | 400 | OSStatus check = SecItemCopyMatching((__bridge CFDictionaryRef)@{ 401 | (__bridge id)kSecClass: (__bridge id)kSecClassKey, 402 | (__bridge id)kSecAttrApplicationTag: tag, 403 | (__bridge id)kSecReturnAttributes: @YES, 404 | }, (CFTypeRef *)&foundItem); 405 | 406 | if (check != errSecSuccess) { 407 | NSLog(@"failed to retrieve key based on new attributes: %d", (int)check); 408 | } 409 | 410 | return check; 411 | } 412 | 413 | - (NSString *) toPublicIdentifier:(NSString *)pubIdentifier 414 | { 415 | return [pubIdentifier stringByAppendingString:@"-pub"]; 416 | } 417 | 418 | - (NSString *) toUUIDIdentifier:(NSString *)privIdentifier 419 | { 420 | return [privIdentifier stringByAppendingString:@"-uuid"]; 421 | } 422 | 423 | NSDictionary* rneccMakeError(NSString* errMsg) 424 | { 425 | return RCTMakeAndLogError(errMsg, nil, nil); 426 | } 427 | 428 | -(NSData *)getPublicKeyDataByLabel:(NSString *)label 429 | { 430 | 431 | NSDictionary* keyAttrs = @{ 432 | (__bridge id)kSecClass: (__bridge id)kSecClassKey, 433 | (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPublic, 434 | (__bridge id)kSecAttrApplicationLabel: label, 435 | (__bridge id)kSecReturnData: @YES, 436 | }; 437 | 438 | CFTypeRef result; 439 | OSStatus sanityCheck = SecItemCopyMatching((__bridge CFDictionaryRef)keyAttrs, &result); 440 | 441 | if (sanityCheck != noErr) 442 | { 443 | return nil; 444 | } 445 | 446 | return CFBridgingRelease(result); 447 | } 448 | 449 | -(SecKeyRef)getKeyRefByLabel:(NSString *)label status:(OSStatus*)status 450 | { 451 | SecKeyRef keyRef; 452 | *status = SecItemCopyMatching((__bridge CFDictionaryRef)@{ 453 | (__bridge id)kSecClass: (__bridge id)kSecClassKey, 454 | (__bridge id)kSecReturnRef: @YES, 455 | (__bridge id)kSecAttrApplicationLabel:label 456 | }, (CFTypeRef *)&keyRef); 457 | 458 | if (*status != errSecSuccess) 459 | { 460 | return nil; 461 | } 462 | 463 | return keyRef; 464 | } 465 | 466 | -(SecKeyRef)getPrivateKeyRef:(nonnull NSDictionary *)options 467 | status:(OSStatus *)status 468 | { 469 | NSMutableDictionary* uuidAttrs = [NSMutableDictionary dictionaryWithDictionary: @{ 470 | (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, 471 | (__bridge id)kSecReturnAttributes: @YES, 472 | }]; 473 | 474 | NSString* serviceID = [options valueForKey:@"service"]; 475 | NSString* base64pub = [options valueForKey:@"pub"]; 476 | 477 | [uuidAttrs setObject:serviceID forKey:(__bridge id)kSecAttrService]; 478 | 479 | NSString* accessGroup = [options valueForKey:@"accessGroup"]; 480 | if (accessGroup) { 481 | [uuidAttrs setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup]; 482 | } 483 | 484 | if (base64pub) { 485 | [uuidAttrs setObject:base64pub forKey:(__bridge id)kSecAttrAccount]; 486 | } 487 | 488 | NSDictionary* found = nil; 489 | CFTypeRef foundTypeRef = NULL; 490 | *status = SecItemCopyMatching((__bridge CFDictionaryRef) uuidAttrs, (CFTypeRef*)&foundTypeRef); 491 | 492 | if (*status != errSecSuccess) { 493 | return nil; 494 | } 495 | 496 | found = (__bridge NSDictionary*)(foundTypeRef); 497 | NSString* uuid = [found objectForKey:(__bridge id)(kSecAttrGeneric)]; 498 | return [self getKeyRefByLabel:uuid status:status]; 499 | } 500 | 501 | -(SecKeyRef)getPublicKeyRef:(NSString *)base64pub 502 | { 503 | NSDictionary* keyAttrs = @{ 504 | (__bridge id)kSecClass: (__bridge id)kSecClassKey, 505 | (__bridge id)kSecReturnRef: @YES, 506 | (__bridge id)kSecAttrApplicationTag: [self toPublicIdentifier:base64pub] 507 | }; 508 | 509 | SecKeyRef keyRef; 510 | OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)keyAttrs, (CFTypeRef *)&keyRef); 511 | if (status != errSecSuccess) 512 | { 513 | return nil; 514 | } 515 | 516 | return keyRef; 517 | } 518 | 519 | - (NSString *)uuidString { 520 | CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault); 521 | NSString *uuidString = (__bridge_transfer NSString *)CFUUIDCreateString(kCFAllocatorDefault, uuid); 522 | CFRelease(uuid); 523 | 524 | return uuidString; 525 | } 526 | 527 | NSString *keychainStatusToString(OSStatus status) { 528 | NSString *message = [NSString stringWithFormat:@"%ld", (long)status]; 529 | 530 | switch (status) { 531 | case errSecSuccess: 532 | message = @"success"; 533 | break; 534 | 535 | case errSecDuplicateItem: 536 | message = @"error item already exists"; 537 | break; 538 | 539 | case errSecItemNotFound : 540 | message = @"error item not found"; 541 | break; 542 | 543 | case errSecAuthFailed: 544 | message = @"error item authentication failed"; 545 | break; 546 | 547 | default: 548 | message = [NSString stringWithFormat:@"error with OSStatus %d", status]; 549 | break; 550 | } 551 | 552 | return message; 553 | } 554 | 555 | @end 556 | -------------------------------------------------------------------------------- /ios/RNECC.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7398B5591C3032A200106C47 /* RNECC.m in Sources */ = {isa = PBXBuildFile; fileRef = 7398B5581C3032A200106C47 /* RNECC.m */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | 7398B5491C30326E00106C47 /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = "include/$(PRODUCT_NAME)"; 18 | dstSubfolderSpec = 16; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 0; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 7398B54B1C30326E00106C47 /* libRNECC.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNECC.a; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 7398B5571C3032A200106C47 /* RNECC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNECC.h; sourceTree = ""; }; 28 | 7398B5581C3032A200106C47 /* RNECC.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RNECC.m; sourceTree = ""; tabWidth = 2; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 7398B5481C30326E00106C47 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 7398B5421C30326E00106C47 = { 43 | isa = PBXGroup; 44 | children = ( 45 | 7398B5571C3032A200106C47 /* RNECC.h */, 46 | 7398B5581C3032A200106C47 /* RNECC.m */, 47 | 7398B54C1C30326E00106C47 /* Products */, 48 | ); 49 | sourceTree = ""; 50 | }; 51 | 7398B54C1C30326E00106C47 /* Products */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | 7398B54B1C30326E00106C47 /* libRNECC.a */, 55 | ); 56 | name = Products; 57 | sourceTree = ""; 58 | }; 59 | /* End PBXGroup section */ 60 | 61 | /* Begin PBXNativeTarget section */ 62 | 7398B54A1C30326E00106C47 /* RNECC */ = { 63 | isa = PBXNativeTarget; 64 | buildConfigurationList = 7398B5541C30326E00106C47 /* Build configuration list for PBXNativeTarget "RNECC" */; 65 | buildPhases = ( 66 | 7398B5471C30326E00106C47 /* Sources */, 67 | 7398B5481C30326E00106C47 /* Frameworks */, 68 | 7398B5491C30326E00106C47 /* CopyFiles */, 69 | ); 70 | buildRules = ( 71 | ); 72 | dependencies = ( 73 | ); 74 | name = RNECC; 75 | productName = RNECC; 76 | productReference = 7398B54B1C30326E00106C47 /* libRNECC.a */; 77 | productType = "com.apple.product-type.library.static"; 78 | }; 79 | /* End PBXNativeTarget section */ 80 | 81 | /* Begin PBXProject section */ 82 | 7398B5431C30326E00106C47 /* Project object */ = { 83 | isa = PBXProject; 84 | attributes = { 85 | LastUpgradeCheck = 0710; 86 | ORGANIZATIONNAME = "Tradle, Inc."; 87 | TargetAttributes = { 88 | 7398B54A1C30326E00106C47 = { 89 | CreatedOnToolsVersion = 7.1.1; 90 | }; 91 | }; 92 | }; 93 | buildConfigurationList = 7398B5461C30326E00106C47 /* Build configuration list for PBXProject "RNECC" */; 94 | compatibilityVersion = "Xcode 3.2"; 95 | developmentRegion = English; 96 | hasScannedForEncodings = 0; 97 | knownRegions = ( 98 | en, 99 | ); 100 | mainGroup = 7398B5421C30326E00106C47; 101 | productRefGroup = 7398B54C1C30326E00106C47 /* Products */; 102 | projectDirPath = ""; 103 | projectRoot = ""; 104 | targets = ( 105 | 7398B54A1C30326E00106C47 /* RNECC */, 106 | ); 107 | }; 108 | /* End PBXProject section */ 109 | 110 | /* Begin PBXSourcesBuildPhase section */ 111 | 7398B5471C30326E00106C47 /* Sources */ = { 112 | isa = PBXSourcesBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | 7398B5591C3032A200106C47 /* RNECC.m in Sources */, 116 | ); 117 | runOnlyForDeploymentPostprocessing = 0; 118 | }; 119 | /* End PBXSourcesBuildPhase section */ 120 | 121 | /* Begin XCBuildConfiguration section */ 122 | 7398B5521C30326E00106C47 /* Debug */ = { 123 | isa = XCBuildConfiguration; 124 | buildSettings = { 125 | ALWAYS_SEARCH_USER_PATHS = NO; 126 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 127 | CLANG_CXX_LIBRARY = "libc++"; 128 | CLANG_ENABLE_MODULES = YES; 129 | CLANG_ENABLE_OBJC_ARC = YES; 130 | CLANG_WARN_BOOL_CONVERSION = YES; 131 | CLANG_WARN_CONSTANT_CONVERSION = YES; 132 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 133 | CLANG_WARN_EMPTY_BODY = YES; 134 | CLANG_WARN_ENUM_CONVERSION = YES; 135 | CLANG_WARN_INT_CONVERSION = YES; 136 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 137 | CLANG_WARN_UNREACHABLE_CODE = YES; 138 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 139 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 140 | COPY_PHASE_STRIP = NO; 141 | DEBUG_INFORMATION_FORMAT = dwarf; 142 | ENABLE_STRICT_OBJC_MSGSEND = YES; 143 | ENABLE_TESTABILITY = YES; 144 | GCC_C_LANGUAGE_STANDARD = gnu99; 145 | GCC_DYNAMIC_NO_PIC = NO; 146 | GCC_NO_COMMON_BLOCKS = YES; 147 | GCC_OPTIMIZATION_LEVEL = 0; 148 | GCC_PREPROCESSOR_DEFINITIONS = ( 149 | "DEBUG=1", 150 | "$(inherited)", 151 | ); 152 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 153 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 154 | GCC_WARN_UNDECLARED_SELECTOR = YES; 155 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 156 | GCC_WARN_UNUSED_FUNCTION = YES; 157 | GCC_WARN_UNUSED_VARIABLE = YES; 158 | HEADER_SEARCH_PATHS = "$(SRCROOT)/../../react-native/React/**"; 159 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 160 | MTL_ENABLE_DEBUG_INFO = YES; 161 | ONLY_ACTIVE_ARCH = YES; 162 | SDKROOT = iphoneos; 163 | }; 164 | name = Debug; 165 | }; 166 | 7398B5531C30326E00106C47 /* Release */ = { 167 | isa = XCBuildConfiguration; 168 | buildSettings = { 169 | ALWAYS_SEARCH_USER_PATHS = NO; 170 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 171 | CLANG_CXX_LIBRARY = "libc++"; 172 | CLANG_ENABLE_MODULES = YES; 173 | CLANG_ENABLE_OBJC_ARC = YES; 174 | CLANG_WARN_BOOL_CONVERSION = YES; 175 | CLANG_WARN_CONSTANT_CONVERSION = YES; 176 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 177 | CLANG_WARN_EMPTY_BODY = YES; 178 | CLANG_WARN_ENUM_CONVERSION = YES; 179 | CLANG_WARN_INT_CONVERSION = YES; 180 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 181 | CLANG_WARN_UNREACHABLE_CODE = YES; 182 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 183 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 184 | COPY_PHASE_STRIP = NO; 185 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 186 | ENABLE_NS_ASSERTIONS = NO; 187 | ENABLE_STRICT_OBJC_MSGSEND = YES; 188 | GCC_C_LANGUAGE_STANDARD = gnu99; 189 | GCC_NO_COMMON_BLOCKS = YES; 190 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 191 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 192 | GCC_WARN_UNDECLARED_SELECTOR = YES; 193 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 194 | GCC_WARN_UNUSED_FUNCTION = YES; 195 | GCC_WARN_UNUSED_VARIABLE = YES; 196 | HEADER_SEARCH_PATHS = "$(SRCROOT)/../../react-native/React/**"; 197 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 198 | MTL_ENABLE_DEBUG_INFO = NO; 199 | SDKROOT = iphoneos; 200 | VALIDATE_PRODUCT = YES; 201 | }; 202 | name = Release; 203 | }; 204 | 7398B5551C30326E00106C47 /* Debug */ = { 205 | isa = XCBuildConfiguration; 206 | buildSettings = { 207 | OTHER_LDFLAGS = "-ObjC"; 208 | PRODUCT_NAME = "$(TARGET_NAME)"; 209 | SKIP_INSTALL = YES; 210 | }; 211 | name = Debug; 212 | }; 213 | 7398B5561C30326E00106C47 /* Release */ = { 214 | isa = XCBuildConfiguration; 215 | buildSettings = { 216 | OTHER_LDFLAGS = "-ObjC"; 217 | PRODUCT_NAME = "$(TARGET_NAME)"; 218 | SKIP_INSTALL = YES; 219 | }; 220 | name = Release; 221 | }; 222 | /* End XCBuildConfiguration section */ 223 | 224 | /* Begin XCConfigurationList section */ 225 | 7398B5461C30326E00106C47 /* Build configuration list for PBXProject "RNECC" */ = { 226 | isa = XCConfigurationList; 227 | buildConfigurations = ( 228 | 7398B5521C30326E00106C47 /* Debug */, 229 | 7398B5531C30326E00106C47 /* Release */, 230 | ); 231 | defaultConfigurationIsVisible = 0; 232 | defaultConfigurationName = Release; 233 | }; 234 | 7398B5541C30326E00106C47 /* Build configuration list for PBXNativeTarget "RNECC" */ = { 235 | isa = XCConfigurationList; 236 | buildConfigurations = ( 237 | 7398B5551C30326E00106C47 /* Debug */, 238 | 7398B5561C30326E00106C47 /* Release */, 239 | ); 240 | defaultConfigurationIsVisible = 0; 241 | defaultConfigurationName = Release; 242 | }; 243 | /* End XCConfigurationList section */ 244 | }; 245 | rootObject = 7398B5431C30326E00106C47 /* Project object */; 246 | } 247 | -------------------------------------------------------------------------------- /ios/RNECC.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/RNECC.xcodeproj/project.xcworkspace/xcuserdata/tenaciousmv.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradle/react-native-ecc/8f69503ad9c63d1628cdc15ab7f00b77d29fa5d6/ios/RNECC.xcodeproj/project.xcworkspace/xcuserdata/tenaciousmv.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /ios/RNECC.xcodeproj/xcuserdata/tenaciousmv.xcuserdatad/xcschemes/RNECC.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /ios/RNECC.xcodeproj/xcuserdata/tenaciousmv.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | RNECC.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 7398B54A1C30326E00106C47 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-ecc", 3 | "version": "2.3.1", 4 | "description": "basic elliptic curve crypto for React Native", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/tradle/react-native-ecc" 11 | }, 12 | "keywords": [ 13 | "react-native", 14 | "react-component", 15 | "ios", 16 | "crypto", 17 | "ecc", 18 | "elliptic", 19 | "sign", 20 | "verify" 21 | ], 22 | "author": "Mark Vayngrib (http://github.com/mvayngrib)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/tradle/react-native-ecc/issues" 26 | }, 27 | "homepage": "https://github.com/tradle/react-native-ecc", 28 | "dependencies": { 29 | "buffer": "^4.9.1", 30 | "hash.js": "^1.0.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /react-native-ecc.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = package['name'] 7 | s.version = package['version'] 8 | s.summary = package['description'] 9 | s.license = package['license'] 10 | 11 | s.authors = package['author'] 12 | s.homepage = package['homepage'] 13 | s.platform = :ios, "9.0" 14 | 15 | s.source = { :git => "https://github.com/tradle/react-native-ecc.git", :tag => "v#{s.version}" } 16 | s.source_files = "ios/**/*.{h,m}" 17 | 18 | s.dependency 'React' 19 | end 20 | --------------------------------------------------------------------------------