├── .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 |
--------------------------------------------------------------------------------