Skip to content

Commit 87f8e51

Browse files
booradjdowning100
andauthored
feat: Add native keccak256 hash function from OpenSSL 3.x (#778)
Co-authored-by: Jonathan Downing <[email protected]>
1 parent 4e0da61 commit 87f8e51

File tree

10 files changed

+380
-150
lines changed

10 files changed

+380
-150
lines changed

bun.lock

Lines changed: 255 additions & 147 deletions
Large diffs are not rendered by default.

example/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ PODS:
3737
- glog
3838
- hermes-engine
3939
- NitroModules
40-
- OpenSSL-Universal
40+
- OpenSSL-Universal (= 3.3.3001)
4141
- RCT-Folly (= 2024.10.14.00)
4242
- RCTRequired
4343
- RCTTypeSafety
@@ -1976,7 +1976,7 @@ SPEC CHECKSUMS:
19761976
hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11
19771977
NitroModules: a93d85b07601390249d7816b59d95afc6317f09d
19781978
OpenSSL-Universal: 6082b0bf950e5636fe0d78def171184e2b3899c2
1979-
QuickCrypto: 3ae297b5c14685bb03a8b2c6a1daef8093feba76
1979+
QuickCrypto: 77dcfb0671e4997d53b89a7d36b42b11d0f8c2c8
19801980
RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17
19811981
RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83
19821982
RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716

example/src/tests/hash/hash_tests.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
createHash,
99
getHashes,
1010
type Encoding,
11+
keccak256,
1112
} from 'react-native-quick-crypto';
1213
import { expect } from 'chai';
1314
import { test } from '../util';
@@ -33,6 +34,46 @@ test(SUITE, 'createHash with null algorithm', () => {
3334
}).to.throw(/Algorithm must be a non-empty string/);
3435
});
3536

37+
test(SUITE, 'check openssl version', () => {
38+
expect(() => {
39+
// Create a hash to trigger OpenSSL initialization
40+
const hash = createHash('sha256');
41+
42+
// Get OpenSSL version directly from the hash object
43+
const version = hash.getOpenSSLVersion();
44+
console.log('OpenSSL Version:', version);
45+
}).to.not.throw();
46+
});
47+
48+
test(SUITE, 'keccak256 function using provider-aware API', () => {
49+
// Test with a simple string
50+
const result1 = keccak256('test');
51+
expect(result1.toString('hex')).to.equal(
52+
'9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658',
53+
);
54+
55+
// Test with empty string
56+
const result2 = keccak256('');
57+
expect(result2.toString('hex')).to.equal(
58+
'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470',
59+
);
60+
61+
// Test with Buffer
62+
const result3 = keccak256(Buffer.from('hello world'));
63+
expect(result3.toString('hex')).to.equal(
64+
'47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad',
65+
);
66+
67+
// Verify the result is 32 bytes (256 bits)
68+
expect(result1.length).to.equal(32);
69+
expect(result2.length).to.equal(32);
70+
expect(result3.length).to.equal(32);
71+
72+
// Test that it's different from SHA3-256 (they should be different)
73+
const sha3Hash = createHash('SHA3-256').update('test').digest();
74+
expect(result1.toString('hex')).to.not.equal(sha3Hash.toString('hex'));
75+
});
76+
3677
// test hashing
3778
const a0 = createHash('md5').update('Test123').digest('latin1');
3879
const a1 = createHash('sha1').update('Test123').digest('hex');

packages/react-native-quick-crypto/QuickCrypto.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,6 @@ Pod::Spec.new do |s|
132132

133133
s.dependency "React-jsi"
134134
s.dependency "React-callinvoker"
135-
s.dependency "OpenSSL-Universal"
135+
s.dependency "OpenSSL-Universal", "3.3.3001"
136136
install_modules_dependencies(s)
137137
end

packages/react-native-quick-crypto/cpp/hash/HybridHash.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include <memory>
33
#include <openssl/err.h>
44
#include <openssl/evp.h>
5+
#include <openssl/opensslv.h>
56
#include <optional>
67
#include <string>
78
#include <vector>
@@ -148,4 +149,44 @@ void HybridHash::setParams() {
148149
}
149150
}
150151

152+
std::string HybridHash::getOpenSSLVersion() {
153+
return OpenSSL_version(OPENSSL_VERSION);
154+
}
155+
156+
std::shared_ptr<ArrayBuffer> HybridHash::keccak256(const std::shared_ptr<ArrayBuffer>& data) {
157+
// 1. Obtain the Keccak-256 message-digest implementation from any loaded provider.
158+
const EVP_MD* md = EVP_MD_fetch(nullptr, "KECCAK-256", nullptr);
159+
if (!md) {
160+
throw std::runtime_error("KECCAK-256 digest not available in the current OpenSSL build (provider not loaded?)");
161+
}
162+
163+
// 2. Create and initialise a digest context.
164+
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
165+
if (!ctx) {
166+
throw std::runtime_error("Failed to allocate EVP_MD_CTX");
167+
}
168+
auto ctx_guard = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(ctx, &EVP_MD_CTX_free);
169+
170+
if (EVP_DigestInit_ex(ctx, md, nullptr) != 1) {
171+
throw std::runtime_error("Failed to initialise KECCAK-256 digest");
172+
}
173+
174+
// 3. Feed the data.
175+
if (EVP_DigestUpdate(ctx, data->data(), data->size()) != 1) {
176+
throw std::runtime_error("Failed to update KECCAK-256 digest");
177+
}
178+
179+
// 4. Finalise and collect the output.
180+
unsigned char hash[EVP_MAX_MD_SIZE];
181+
unsigned int out_len = 0;
182+
if (EVP_DigestFinal_ex(ctx, hash, &out_len) != 1) {
183+
throw std::runtime_error("Failed to finalise KECCAK-256 digest");
184+
}
185+
186+
// 5. Move the result into a managed ArrayBuffer.
187+
unsigned char* out_buf = new unsigned char[out_len];
188+
std::memcpy(out_buf, hash, out_len);
189+
return std::make_shared<NativeArrayBuffer>(out_buf, out_len, [out_buf]() { delete[] out_buf; });
190+
}
191+
151192
} // namespace margelo::nitro::crypto

packages/react-native-quick-crypto/cpp/hash/HybridHash.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class HybridHash : public HybridHashSpec {
2525
std::shared_ptr<ArrayBuffer> digest(const std::optional<std::string>& encoding = std::nullopt) override;
2626
std::shared_ptr<margelo::nitro::crypto::HybridHashSpec> copy(const std::optional<double> outputLength) override;
2727
std::vector<std::string> getSupportedHashAlgorithms() override;
28+
std::string getOpenSSLVersion() override;
29+
std::shared_ptr<ArrayBuffer> keccak256(const std::shared_ptr<ArrayBuffer>& data) override;
2830

2931
private:
3032
// Methods

packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHashSpec.cpp

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridHashSpec.hpp

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-native-quick-crypto/src/hash.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,22 @@ class HashUtils {
1010
public static getSupportedHashAlgorithms(): string[] {
1111
return this.native.getSupportedHashAlgorithms();
1212
}
13+
public static keccak256(data: BinaryLike): Buffer {
14+
const nativeDigest = this.native.keccak256(
15+
binaryLikeToArrayBuffer(data, 'utf8'),
16+
);
17+
return Buffer.from(nativeDigest);
18+
}
1319
}
1420

1521
export function getHashes() {
1622
return HashUtils.getSupportedHashAlgorithms();
1723
}
1824

25+
export function keccak256(data: BinaryLike): Buffer {
26+
return HashUtils.keccak256(data);
27+
}
28+
1929
interface HashOptions extends TransformOptions {
2030
/**
2131
* For XOF hash functions such as `shake256`, the
@@ -157,6 +167,27 @@ class Hash extends Stream.Transform {
157167
return hash;
158168
}
159169

170+
/**
171+
* Returns the OpenSSL version string
172+
* @since v1.0.0
173+
*/
174+
getOpenSSLVersion(): string {
175+
return this.native.getOpenSSLVersion();
176+
}
177+
178+
/**
179+
* Computes KECCAK-256 hash of the provided data using OpenSSL's provider-aware API
180+
* @since v1.0.0
181+
* @param data The data to hash
182+
* @returns Buffer containing the KECCAK-256 hash
183+
*/
184+
keccak256(data: BinaryLike): Buffer {
185+
const nativeDigest = this.native.keccak256(
186+
binaryLikeToArrayBuffer(data, 'utf8'),
187+
);
188+
return Buffer.from(nativeDigest);
189+
}
190+
160191
// stream interface
161192
_transform(
162193
chunk: BinaryLike,
@@ -205,4 +236,5 @@ export function createHash(algorithm: string, options?: HashOptions): Hash {
205236
export const hashExports = {
206237
createHash,
207238
getHashes,
239+
keccak256,
208240
};

packages/react-native-quick-crypto/src/specs/hash.nitro.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ export interface Hash extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
66
digest(encoding?: string): ArrayBuffer;
77
copy(outputLength?: number): Hash;
88
getSupportedHashAlgorithms(): string[];
9+
getOpenSSLVersion(): string;
10+
keccak256(data: ArrayBuffer): ArrayBuffer;
911
}

0 commit comments

Comments
 (0)