-
Notifications
You must be signed in to change notification settings - Fork 118
feat: implement scrypt key derivation function
#849
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import rnqc from 'react-native-quick-crypto'; | ||
| import * as noble from '@noble/hashes/scrypt'; | ||
| // @ts-expect-error - crypto-browserify is not typed | ||
| import browserify from 'crypto-browserify'; | ||
| import type { BenchFn } from '../../types/benchmarks'; | ||
| import { Bench } from 'tinybench'; | ||
|
|
||
| const TIME_MS = 1000; | ||
|
|
||
| // N=256, r=8, p=1 is light and fast enough for mobile benchmarking | ||
| // Higher values like 1024 can cause timeouts on slower devices | ||
| const N = 256; | ||
| const r = 8; | ||
| const p = 1; | ||
| const keylen = 64; | ||
|
|
||
| const scrypt_async: BenchFn = () => { | ||
| const bench = new Bench({ | ||
| name: `scrypt N=${N} r=${r} p=${p} (async)`, | ||
| time: TIME_MS, | ||
| }); | ||
|
|
||
| bench | ||
| .add('rnqc', async () => { | ||
| try { | ||
| await new Promise<void>((resolve, reject) => { | ||
| rnqc.scrypt( | ||
| 'password', | ||
| 'salt', | ||
| keylen, | ||
| { N, r, p, maxmem: 32 * 1024 * 1024 }, | ||
| (err: unknown) => { | ||
| if (err) reject(err); | ||
| else resolve(); | ||
| }, | ||
| ); | ||
| }); | ||
| } catch (error) { | ||
| console.error('RNQC scrypt error:', error); | ||
| throw error; | ||
| } | ||
| }) | ||
| .add('@noble/hashes/scrypt', async () => { | ||
| await noble.scryptAsync('password', 'salt', { N, r, p, dkLen: keylen }); | ||
| }) | ||
| .add('browserify/scrypt', async () => { | ||
| await new Promise<void>((resolve, reject) => { | ||
| browserify.scrypt( | ||
| 'password', | ||
| 'salt', | ||
| keylen, | ||
| { N, r, p }, | ||
| (err: unknown) => { | ||
| if (err) reject(err); | ||
| else resolve(); | ||
| }, | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| bench.warmupTime = 100; | ||
| return bench; | ||
| }; | ||
|
|
||
| const scrypt_sync: BenchFn = () => { | ||
| const bench = new Bench({ | ||
| name: `scrypt N=${N} r=${r} p=${p} (sync)`, | ||
| time: TIME_MS, | ||
| }); | ||
|
|
||
| bench | ||
| .add('rnqc', () => { | ||
| try { | ||
| rnqc.scryptSync('password', 'salt', keylen, { | ||
| N, | ||
| r, | ||
| p, | ||
| maxmem: 32 * 1024 * 1024, | ||
| }); | ||
| } catch (error) { | ||
| console.error('RNQC scryptSync error:', error); | ||
| throw error; | ||
| } | ||
| }) | ||
| .add('@noble/hashes/scrypt', () => { | ||
| noble.scrypt('password', 'salt', { N, r, p, dkLen: keylen }); | ||
| }) | ||
| .add('browserify/scrypt', () => { | ||
| browserify.scryptSync('password', 'salt', keylen, { N, r, p }); | ||
| }); | ||
|
|
||
| bench.warmupTime = 100; | ||
| return bench; | ||
| }; | ||
|
|
||
| export default [scrypt_async, scrypt_sync]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| /* eslint-disable @typescript-eslint/no-unused-expressions */ | ||
| import { Buffer } from 'safe-buffer'; | ||
| import { expect } from 'chai'; | ||
| import { test } from '../util'; | ||
|
|
||
| import crypto from 'react-native-quick-crypto'; | ||
|
|
||
| const SUITE = 'scrypt'; | ||
|
|
||
| // RFC 7914 Test Vectors | ||
| // https://tools.ietf.org/html/rfc7914#section-2 | ||
| const kTests = [ | ||
| { | ||
| password: '', | ||
| salt: '', | ||
| N: 16, | ||
| r: 1, | ||
| p: 1, | ||
| keylen: 64, | ||
| expected: | ||
| '77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906', | ||
| }, | ||
| { | ||
| password: 'password', | ||
| salt: 'NaCl', | ||
| N: 1024, | ||
| r: 8, | ||
| p: 16, | ||
| keylen: 64, | ||
| expected: | ||
| 'fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640', | ||
| }, | ||
| { | ||
| password: 'pleaseletmein', | ||
| salt: 'SodiumChloride', | ||
| N: 16384, | ||
| r: 8, | ||
| p: 1, | ||
| keylen: 64, | ||
| expected: | ||
| '7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887', | ||
| }, | ||
| ]; | ||
|
|
||
| kTests.forEach(({ password, salt, N, r, p, keylen, expected }, index) => { | ||
| const description = `RFC 7914 Test Case ${index + 1}`; | ||
|
|
||
| test(SUITE, `${description} (async)`, () => { | ||
| crypto.scrypt( | ||
| password, | ||
| salt, | ||
| keylen, | ||
| { N, r, p, maxmem: 32 * 1024 * 1024 }, // 32MB - generous headroom for all test cases | ||
| (err, derivedKey) => { | ||
| expect(err).to.be.null; | ||
| expect(derivedKey).not.to.be.undefined; | ||
| expect(derivedKey!.toString('hex')).to.equal(expected); | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| test(SUITE, `${description} (sync)`, () => { | ||
| const derivedKey = crypto.scryptSync(password, salt, keylen, { | ||
| N, | ||
| r, | ||
| p, | ||
| maxmem: 32 * 1024 * 1024, // 32MB - generous headroom for all test cases | ||
| }); | ||
| expect(derivedKey).not.to.be.undefined; | ||
| expect(derivedKey.toString('hex')).to.equal(expected); | ||
| }); | ||
| }); | ||
|
|
||
| test(SUITE, 'should throw if no callback provided (async)', () => { | ||
| expect(() => { | ||
| crypto.scrypt('password', 'salt', 64); | ||
| }).to.throw(/No callback provided/); | ||
| }); | ||
|
|
||
| test(SUITE, 'should handle default options (async)', () => { | ||
| // This just tests it doesn't crash and returns a buffer | ||
| crypto.scrypt('password', 'salt', 32, (err, key) => { | ||
| expect(err).to.be.null; | ||
| expect(key).to.be.instanceOf(Buffer); | ||
| expect(key!.length).to.equal(32); | ||
| }); | ||
| }); | ||
|
|
||
| test(SUITE, 'should handle aliases cost/blockSize/parallelization', () => { | ||
| // Same as Test Case 1 but with named aliases | ||
| const t = kTests[0]!; | ||
| const derivedKey = crypto.scryptSync(t.password, t.salt, t.keylen, { | ||
| cost: t.N, | ||
| blockSize: t.r, | ||
| parallelization: t.p, | ||
| }); | ||
| expect(derivedKey.toString('hex')).to.equal(t.expected); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
packages/react-native-quick-crypto/cpp/scrypt/HybridScrypt.cpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| #include <NitroModules/ArrayBuffer.hpp> | ||
| #include <memory> | ||
| #include <openssl/err.h> | ||
| #include <openssl/evp.h> | ||
| #include <string> | ||
| #include <vector> | ||
|
|
||
| #include "HybridScrypt.hpp" | ||
| #include "Utils.hpp" | ||
|
|
||
| namespace margelo::nitro::crypto { | ||
|
|
||
| std::shared_ptr<Promise<std::shared_ptr<ArrayBuffer>>> HybridScrypt::deriveKey(const std::shared_ptr<ArrayBuffer>& password, | ||
| const std::shared_ptr<ArrayBuffer>& salt, double N, double r, | ||
| double p, double maxmem, double keylen) { | ||
| // get owned NativeArrayBuffers before passing to sync function | ||
| auto nativePassword = ToNativeArrayBuffer(password); | ||
| auto nativeSalt = ToNativeArrayBuffer(salt); | ||
|
|
||
| return Promise<std::shared_ptr<ArrayBuffer>>::async([this, nativePassword, nativeSalt, N, r, p, maxmem, keylen]() { | ||
| return this->deriveKeySync(nativePassword, nativeSalt, N, r, p, maxmem, keylen); | ||
| }); | ||
| } | ||
|
|
||
| std::shared_ptr<ArrayBuffer> HybridScrypt::deriveKeySync(const std::shared_ptr<ArrayBuffer>& password, | ||
| const std::shared_ptr<ArrayBuffer>& salt, double N, double r, double p, | ||
| double maxmem, double keylen) { | ||
| // Use EVP_PBE_scrypt to match Node.js implementation exactly | ||
| // All parameters are uint64_t for this API (unlike EVP_KDF which uses uint32_t for r/p) | ||
| uint64_t n_val = static_cast<uint64_t>(N); | ||
| uint64_t r_val = static_cast<uint64_t>(r); | ||
| uint64_t p_val = static_cast<uint64_t>(p); | ||
| uint64_t maxmem_val = static_cast<uint64_t>(maxmem); | ||
| size_t outLen = static_cast<size_t>(keylen); | ||
|
|
||
| if (outLen == 0) { | ||
| throw std::runtime_error("SCRYPT length cannot be zero"); | ||
| } | ||
|
|
||
| // Prepare password and salt pointers | ||
| const char* pass_data = password && password->size() > 0 ? reinterpret_cast<const char*>(password->data()) : ""; | ||
| size_t pass_len = password ? password->size() : 0; | ||
|
|
||
| const unsigned char* salt_data = | ||
| salt && salt->size() > 0 ? reinterpret_cast<const unsigned char*>(salt->data()) : reinterpret_cast<const unsigned char*>(""); | ||
| size_t salt_len = salt ? salt->size() : 0; | ||
|
|
||
| // Allocate output buffer | ||
| uint8_t* outBuf = new uint8_t[outLen]; | ||
|
|
||
| // Use EVP_PBE_scrypt - the same API Node.js uses | ||
| int result = EVP_PBE_scrypt(pass_data, pass_len, salt_data, salt_len, n_val, r_val, p_val, maxmem_val, outBuf, outLen); | ||
|
|
||
| if (result != 1) { | ||
| delete[] outBuf; | ||
| throw std::runtime_error("SCRYPT derivation failed: " + getOpenSSLError()); | ||
| } | ||
|
|
||
| return std::make_shared<NativeArrayBuffer>(outBuf, outLen, [=]() { delete[] outBuf; }); | ||
| } | ||
|
|
||
| } // namespace margelo::nitro::crypto |
28 changes: 28 additions & 0 deletions
28
packages/react-native-quick-crypto/cpp/scrypt/HybridScrypt.hpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| #pragma once | ||
|
|
||
| #include <NitroModules/ArrayBuffer.hpp> | ||
| #include <NitroModules/Promise.hpp> | ||
| #include <memory> | ||
| #include <openssl/evp.h> | ||
| #include <string> | ||
|
|
||
| #include "HybridScryptSpec.hpp" | ||
|
|
||
| namespace margelo::nitro::crypto { | ||
|
|
||
| using namespace facebook; | ||
|
|
||
| class HybridScrypt : public HybridScryptSpec { | ||
| public: | ||
| HybridScrypt() : HybridObject(TAG) {} | ||
|
|
||
| public: | ||
| // Methods | ||
| std::shared_ptr<ArrayBuffer> deriveKeySync(const std::shared_ptr<ArrayBuffer>& password, const std::shared_ptr<ArrayBuffer>& salt, | ||
| double N, double r, double p, double maxmem, double keylen) override; | ||
| std::shared_ptr<Promise<std::shared_ptr<ArrayBuffer>>> deriveKey(const std::shared_ptr<ArrayBuffer>& password, | ||
| const std::shared_ptr<ArrayBuffer>& salt, double N, double r, double p, | ||
| double maxmem, double keylen) override; | ||
| }; | ||
|
|
||
| } // namespace margelo::nitro::crypto |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
10 changes: 10 additions & 0 deletions
10
packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.