diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6432642 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +.idea + diff --git a/CHANGELOG b/CHANGELOG index 8adff1b..291b3c4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ Changelog ========= +UNRELEASED - 2020-10-04 +------------------ +- Unified native I/O to hex strings ; +- Added other encoding methods : 'legacy' which behaves like v1.1.2, 'hex', 'base64' and 'buffer' +- Update example with tests for a range of test vectors + 1.1.2 - 2019-12-20 ------------------ @@ -17,4 +23,4 @@ Changelog [danh] - Update iOS config. - [maddijoyce] \ No newline at end of file + [maddijoyce] diff --git a/README.md b/README.md index 744e087..bdcad5d 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,14 @@ This plugin is for use with React Native and allows your application to use scry ```javascript import scrypt from 'react-native-scrypt'; -// passwd must be a string -// salt must be an array of bytes integers +// With 'legacy' encoding (default): passwd must be a string, salt must be an array of bytes integers +// With 'hex' encoding: passwd and salt must be string encoded in hexadecimal +// With 'base64' encoding: passwd and salt must be string encoded in base64 +// With 'buffer' encoding: passwd and salt must be Buffers (in the sense of [`buffer`](https://github.com/feross/buffer/) package) // see example/App.js -const result = await scrypt(passwd, salt[, N=16384, r=8, p=1, dkLen=64]) + +const result = await scrypt(passwd, salt[, N=16384, r=8, p=1, dkLen=64, encoding='legacy']) ``` ## LICENSE diff --git a/android/src/main/java/com/crypho/scrypt/RNScryptModule.java b/android/src/main/java/com/crypho/scrypt/RNScryptModule.java index b26f789..f349b7f 100644 --- a/android/src/main/java/com/crypho/scrypt/RNScryptModule.java +++ b/android/src/main/java/com/crypho/scrypt/RNScryptModule.java @@ -13,60 +13,59 @@ public class RNScryptModule extends ReactContextBaseJavaModule { System.loadLibrary("scrypt_jni"); } - private static final char[] HEX = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; + private static final char[] HEX = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; private final ReactApplicationContext reactContext; private static final String SCRYPT_ERROR = "Failure in scrypt"; - public native byte[] scryptBridgeJNI(byte[] pass, char[] salt, Integer N, Integer r, Integer p, Integer dkLen); - + public native byte[] scryptBridgeJNI(byte[] pass, byte[] salt, Integer N, Integer r, Integer p, Integer dkLen); + public RNScryptModule(ReactApplicationContext reactContext) { super(reactContext); this.reactContext = reactContext; } - @ReactMethod public void scrypt( - String passwd, - ReadableArray salt, - Integer N, - Integer r, - Integer p, - Integer dkLen, - Promise promise) { - try { - final byte[] passwordBytes = passwd.getBytes("UTF-8"); - final char[] ssalt = getSalt(salt); - byte[] res = scryptBridgeJNI(passwordBytes, ssalt, N, r, p, dkLen); - String result = hexify(res); - promise.resolve(result); - } catch (Exception e) { - promise.reject(SCRYPT_ERROR, e); - } + String passwd, + String salt, + Integer N, + Integer r, + Integer p, + Integer dkLen, + Promise promise) { + try { + final byte[] passwordBytes = hexStringToByteArray(passwd); + final byte[] ssalt = hexStringToByteArray(salt); + byte[] res = scryptBridgeJNI(passwordBytes, ssalt, N, r, p, dkLen); + String result = hexify(res); + promise.resolve(result); + } catch (Exception e) { + promise.reject(SCRYPT_ERROR, e); + } } - private String hexify (byte[] input) { - int len = input.length; - char[] result = new char[2 * len]; - for ( int j = 0; j < len; j++ ) { - int v = input[j] & 0xFF; - result[j * 2] = HEX[v >>> 4]; - result[j * 2 + 1] = HEX[v & 0x0F]; + private static String hexify(byte[] input) { + int len = input.length; + char[] result = new char[2 * len]; + for (int j = 0; j < len; j++) { + int v = input[j] & 0xFF; + result[j * 2] = HEX[v >>> 4]; + result[j * 2 + 1] = HEX[v & 0x0F]; } return new String(result).toLowerCase(); } - private char[] getSalt(ReadableArray src){ - int s = src.size(); - char[] result = new char[s]; - for (int i = 0; i < s ; i++) { - result[i] = (char) src.getInt(i); + private static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); } - return result; + return data; } @Override public String getName() { return "RNScrypt"; } -} \ No newline at end of file +} diff --git a/android/src/main/jni/libscrypt-jni.c b/android/src/main/jni/libscrypt-jni.c index c061210..c55d315 100644 --- a/android/src/main/jni/libscrypt-jni.c +++ b/android/src/main/jni/libscrypt-jni.c @@ -19,7 +19,7 @@ static void throwException(JNIEnv* env, char *msg); JNIEXPORT jbyteArray JNICALL Java_com_crypho_scrypt_RNScryptModule_scryptBridgeJNI( JNIEnv* env, jobject thiz, - jbyteArray pass, jcharArray salt, jobject N, jobject r, jobject p, jobject dkLen) + jbyteArray pass, jbyteArray salt, jobject N, jobject r, jobject p, jobject dkLen) { int i; char *msg_error; @@ -31,13 +31,13 @@ Java_com_crypho_scrypt_RNScryptModule_scryptBridgeJNI( JNIEnv* env, jobject thiz jint passLen = (*env)->GetArrayLength(env, pass); if((*env)->ExceptionOccurred(env)) { - LOGE("Failed to get passphrase lenght."); + LOGE("Failed to get passphrase length."); goto END; } jint saltLen = (*env)->GetArrayLength(env, salt); if((*env)->ExceptionOccurred(env)) { - LOGE("Failed to get salt lenght."); + LOGE("Failed to get salt length."); goto END; } @@ -47,20 +47,12 @@ Java_com_crypho_scrypt_RNScryptModule_scryptBridgeJNI( JNIEnv* env, jobject thiz goto END; } - jchar *salt_chars = (*env)->GetCharArrayElements(env, salt, NULL); + jbyte *parsedSalt = (*env)->GetByteArrayElements(env, salt, NULL); if((*env)->ExceptionOccurred(env)) { LOGE("Failed to get salt elements."); goto END; } - uint8_t *parsedSalt = malloc(sizeof(uint8_t) * saltLen); - if (parsedSalt == NULL) { - msg_error = "Failed to malloc parsedSalt."; - LOGE("%s", msg_error); - throwException(env, msg_error); - goto END; - } - uint8_t *hashbuf = malloc(sizeof(uint8_t) * dkLen_i); if (hashbuf == NULL) { msg_error = "Failed to malloc hashbuf."; @@ -69,10 +61,6 @@ Java_com_crypho_scrypt_RNScryptModule_scryptBridgeJNI( JNIEnv* env, jobject thiz goto END; } - for (i = 0; i < saltLen; ++i) { - parsedSalt[i] = (uint8_t) salt_chars[i]; - } - if (libscrypt_scrypt(passphrase, passLen, parsedSalt, saltLen, N_i, r_i, p_i, hashbuf, dkLen_i)) { switch (errno) { case EINVAL: @@ -103,9 +91,8 @@ Java_com_crypho_scrypt_RNScryptModule_scryptBridgeJNI( JNIEnv* env, jobject thiz END: if (passphrase) (*env)->ReleaseByteArrayElements(env, pass, passphrase, JNI_ABORT); - if (salt_chars) (*env)->ReleaseCharArrayElements(env, salt, salt_chars, JNI_ABORT); + if (parsedSalt) (*env)->ReleaseByteArrayElements(env, salt, parsedSalt, JNI_ABORT); if (hashbuf) free(hashbuf); - if (parsedSalt) free(parsedSalt); return result; } diff --git a/android/src/main/libs/arm64-v8a/libscrypt_jni.so b/android/src/main/libs/arm64-v8a/libscrypt_jni.so index 1abe5f2..16c4c7f 100755 Binary files a/android/src/main/libs/arm64-v8a/libscrypt_jni.so and b/android/src/main/libs/arm64-v8a/libscrypt_jni.so differ diff --git a/android/src/main/libs/armeabi-v7a/libscrypt_jni.so b/android/src/main/libs/armeabi-v7a/libscrypt_jni.so index 2e1de04..d18cf45 100755 Binary files a/android/src/main/libs/armeabi-v7a/libscrypt_jni.so and b/android/src/main/libs/armeabi-v7a/libscrypt_jni.so differ diff --git a/android/src/main/libs/x86/libscrypt_jni.so b/android/src/main/libs/x86/libscrypt_jni.so index 3c12e92..6076bbb 100755 Binary files a/android/src/main/libs/x86/libscrypt_jni.so and b/android/src/main/libs/x86/libscrypt_jni.so differ diff --git a/android/src/main/libs/x86_64/libscrypt_jni.so b/android/src/main/libs/x86_64/libscrypt_jni.so index 2e3fff4..e8a9132 100755 Binary files a/android/src/main/libs/x86_64/libscrypt_jni.so and b/android/src/main/libs/x86_64/libscrypt_jni.so differ diff --git a/example/.prettierrc.js b/example/.prettierrc.js index 6015616..8338db4 100644 --- a/example/.prettierrc.js +++ b/example/.prettierrc.js @@ -4,4 +4,5 @@ module.exports = { singleQuote: true, trailingComma: 'all', semi: false, + printWidth: 120 } diff --git a/example/App.js b/example/App.js index 5a49975..c8aaf7f 100644 --- a/example/App.js +++ b/example/App.js @@ -7,18 +7,21 @@ */ import React, {Component} from 'react' -import {StyleSheet, Text, View} from 'react-native' +import {ActivityIndicator, Button, StyleSheet, Text, View} from 'react-native' import scrypt from 'react-native-scrypt' import sjcl from './sjcl' - -const password = 'correct horse battery staple' -const params = [16384, 8, 1] -const length = 64 +import {randomBytes} from 'react-native-randombytes' +import {assert} from 'chai' +import {Buffer} from 'buffer' +import testVectors from './test_vectors' function seeded() { return new Promise(resolve => { - if (sjcl.random.isReady()) resolve() - else sjcl.random.addEventListener('seeded', resolve) + if (sjcl.random.isReady()) { + resolve() + } else { + sjcl.random.addEventListener('seeded', resolve) + } }) } @@ -28,34 +31,156 @@ export default class App extends Component { this.state = { sjcl: '...', libscrypt: '...', + ready: false, + tests: 0, + passedTests: 0, + } + } + + async test(name, callback) { + console.log(`Executing ${name}...`) + const start = Date.now() + try { + await callback() + console.log(`Test ${name} passed in ${Date.now() - start}ms`) + this.setState({tests: this.state.tests + 1, passedTests: this.state.passedTests + 1}) + } catch (error) { + console.warn(`Test ${name} failed in ${Date.now() - start}ms with error`, error) + this.setState({tests: this.state.tests + 1}) } } async componentDidMount() { await seeded() - const salt = sjcl.random.randomWords(2) - await this.libScrypt(salt) - await this.sjclScrypt(salt) + await this.generateRandomTestVector() + await this.runExample() } - async sjclScrypt(salt) { - const start = Date.now() - const sjclScrypt = sjcl.codec.hex.fromBits( - sjcl.misc.scrypt(password, salt, ...params, length * 8), + async generateRandomTestVector() { + // add a random test vector with non-ASCII characters + console.log('generating random test vector') + const testVector = { + password: await randomBytes(64), + salt: await randomBytes(64), + N: 16384, + r: 8, + p: 1, + dkLen: 64, + skipInLegacy: true, + } + testVector.expected = Buffer.from( + sjcl.codec.hex.fromBits( + sjcl.misc.scrypt( + sjcl.codec.hex.toBits(testVector.password.toString('hex')), + sjcl.codec.hex.toBits(testVector.salt.toString('hex')), + testVector.N, + testVector.r, + testVector.p, + testVector.dkLen * 8, + ), + ), + 'hex', ) - this.setState({sjcl: `${sjclScrypt} ${Date.now() - start}ms`}) - return sjclScrypt + testVectors.push(testVector) + console.log('random test vector generated') } - async libScrypt(salt) { + async runExample() { + console.log('running quick example', testVectors.length) + await this.sjclScrypt(testVectors[21]) + console.log('control sjcl scrypt done') + await this.libScrypt(testVectors[21]) + console.log('native scrypt done') + this.setState({ready: true}) + } + + async startTests() { + if (this.state.ready === false) { + return + } + this.setState({ready: false, tests: 0, passedTests: 0}) + console.log('starting tests...') + + for (let i = 0; i < testVectors.length; i++) { + const {password, salt, N, r, p, dkLen, expected, skipInLegacy = false} = testVectors[i] + if (!skipInLegacy) { + await this.test(`Test vector ${i + 1} with encoding legacy`, async () => { + const encodedPassword = password.toString('utf-8') + const encodedSalt = sjcl.codec.utf8String.toBits(salt.toString('utf-8')) + const actualResult = await scrypt(encodedPassword, encodedSalt, N, r, p, dkLen) + assert.strictEqual(actualResult, expected.toString('hex')) + }) + } + } + + for (let i = 0; i < testVectors.length; i++) { + const {password, salt, N, r, p, dkLen, expected} = testVectors[i] + await this.test(`Test vector ${i + 1} with encoding base64`, async () => { + const encodedPassword = password.toString('base64') + const encodedSalt = salt.toString('base64') + const actualResult = await scrypt(encodedPassword, encodedSalt, N, r, p, dkLen, 'base64') + assert.strictEqual(actualResult, expected.toString('base64')) + }) + } + + for (let i = 0; i < testVectors.length; i++) { + const {password, salt, N, r, p, dkLen, expected} = testVectors[i] + await this.test(`Test vector ${i + 1} with encoding hex`, async () => { + const encodedPassword = password.toString('hex') + const encodedSalt = salt.toString('hex') + const actualResult = await scrypt(encodedPassword, encodedSalt, N, r, p, dkLen, 'hex') + assert.strictEqual(actualResult, expected.toString('hex')) + }) + } + + for (let i = 0; i < testVectors.length; i++) { + const {password, salt, N, r, p, dkLen, expected} = testVectors[i] + await this.test(`Test vector ${i + 1} with encoding buffer`, async () => { + const encodedPassword = password + const encodedSalt = salt + const actualResult = await scrypt(encodedPassword, encodedSalt, N, r, p, dkLen, 'buffer') + assert.isTrue(actualResult.equals(expected)) + }) + } + + if (this.state.tests === this.state.passedTests) { + console.log(`All ${this.state.tests} passed.`) + } else { + console.error( + `${this.state.passedTests} / ${this.state.tests} passed, ${this.state.tests - this.state.passedTests} failed.`, + ) + } + this.setState({ready: true}) + } + + async sjclScrypt(testVector) { + const password = sjcl.codec.hex.toBits(testVector.password.toString('hex')) + const salt = sjcl.codec.hex.toBits(testVector.salt.toString('hex')) + const N = testVector.N + const r = testVector.r + const p = testVector.p + const dkLen = testVector.dkLen * 8 + const start = Date.now() - const libScrypt = await scrypt( - password, - sjcl.codec.bytes.fromBits(salt), - ...params, - length, - ) - this.setState({libscrypt: `${libScrypt} ${Date.now() - start}ms`}) + const sjclScrypt = sjcl.misc.scrypt(password, salt, N, r, p, dkLen) + const elapsed = Date.now() - start + + this.setState({sjcl: `${sjcl.codec.hex.fromBits(sjclScrypt)} ${elapsed}ms`}) + } + + async libScrypt(testVector) { + const password = testVector.password.toString('utf-8') + const salt = sjcl.codec.hex.toBits(testVector.salt.toString('hex')) + const N = testVector.N + const r = testVector.r + const p = testVector.p + const dkLen = testVector.dkLen + + const start = Date.now() + const libScrypt = await scrypt(password, salt, N, r, p, dkLen) + const elapsed = Date.now() - start + + this.setState({libscrypt: `${libScrypt} ${elapsed}ms`}) return libScrypt } @@ -63,9 +188,11 @@ export default class App extends Component { return ( {`sjcl: ${this.state.sjcl}`} - - {`react-native-scrypt: ${this.state.libscrypt}`} - + {`react-native-scrypt: ${this.state.libscrypt}`} +