Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ Compatibility Table
## Usage

```javascript
import argon2 from 'react-native-argon2';
import argon2, { verify } from 'react-native-argon2';
const password = 'password';
const salt = '1234567891011121314151617181920212223242526272829303132333435363';
const result = await argon2(password, salt, {});
const { rawHash, encodedHash } = result;
// rawHash: 031d6c82ddede1200f4794605052745dd562bd4db358e23dac1b11c052eff8d9
// encodedHash: $argon2id$v=19$m=32768,t=2,p=1$MTIzNDU2Nzg5MTAxMTEyMTMxNDE1MTYxNzE4MTkyMDIxMjIyMzI0MjUyNjI3MjgyOTMwMzEzMjMzMzQzNTM2Mw$Ax1sgt3t4SAPR5RgUFJ0XdVivU2zWOI9rBsRwFLv+Nk

// mode is auto-detected from the encoded hash prefix ($argon2id$, $argon2i$, $argon2d$)
const isValid = await verify(password, encodedHash);
// isValid: true
```

### Input
Expand Down Expand Up @@ -61,9 +65,24 @@ const result = await argon2(
);
```


### Output

`rawHash` is the hexadecimal representation

`encodedHash` is the string representation

### Verify

Use `verify(password, encodedHash)` to check whether a password matches an existing Argon2 hash.

The mode (`argon2id`, `argon2i`, `argon2d`) is automatically inferred from the encoded hash prefix on both iOS and Android.

```javascript
import { verify } from 'react-native-argon2';

const isValid = await verify('password', '$argon2id$v=19$m=32768,t=2,p=1$...');
```

### Verify Output

`isValid` is a boolean that indicates whether the password matches the encoded hash.
215 changes: 125 additions & 90 deletions android/src/main/java/com/poowf/argon2/RNArgon2Module.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,118 +2,153 @@

import android.app.Activity;
import android.content.Intent;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.bridge.Promise;

import com.lambdapioneer.argon2kt.Argon2Kt;
import com.lambdapioneer.argon2kt.Argon2KtResult;
import com.lambdapioneer.argon2kt.Argon2Mode;

public class RNArgon2Module extends ReactContextBaseJavaModule {
private ReactContext mReactContext;
private ReactContext mReactContext;

public RNArgon2Module(ReactApplicationContext reactContext) {
super(reactContext);
mReactContext = reactContext;
}

@Override
public String getName() {
return "RNArgon2";
}

public RNArgon2Module(ReactApplicationContext reactContext) {
super(reactContext);
mReactContext = reactContext;
private byte[] hexStringToByteArray(String hex) {
if (hex == null || hex.isEmpty()) {
throw new IllegalArgumentException("Hex salt cannot be null or empty");
}

@Override
public String getName() {
return "RNArgon2";
// Validate hex format
if (hex.length() % 2 != 0) {
throw new IllegalArgumentException(
"Hex salt must have even length, got: " + hex.length());
}

private byte[] hexStringToByteArray(String hex) {
if (hex == null || hex.isEmpty()) {
throw new IllegalArgumentException("Hex salt cannot be null or empty");
}

// Validate hex format
if (hex.length() % 2 != 0) {
throw new IllegalArgumentException("Hex salt must have even length, got: " + hex.length());
}

int len = hex.length();
byte[] result = new byte[len / 2];

try {
for (int i = 0; i < len; i += 2) {
int high = Character.digit(hex.charAt(i), 16);
int low = Character.digit(hex.charAt(i + 1), 16);

if (high == -1 || low == -1) {
throw new IllegalArgumentException("Invalid hex character at position " + i + " in: " + hex);
}

result[i / 2] = (byte) ((high << 4) + low);
}
} catch (StringIndexOutOfBoundsException e) {
throw new IllegalArgumentException("Malformed hex string: " + hex, e);
int len = hex.length();
byte[] result = new byte[len / 2];

try {
for (int i = 0; i < len; i += 2) {
int high = Character.digit(hex.charAt(i), 16);
int low = Character.digit(hex.charAt(i + 1), 16);

if (high == -1 || low == -1) {
throw new IllegalArgumentException(
"Invalid hex character at position " + i + " in: " + hex);
}

return result;

result[i / 2] = (byte)((high << 4) + low);
}
} catch (StringIndexOutOfBoundsException e) {
throw new IllegalArgumentException("Malformed hex string: " + hex, e);
}

@ReactMethod
public void argon2(String password, String salt, ReadableMap config, Promise promise) {
try {
final byte[] passwordBytes = password.getBytes("UTF-8");

String saltEncoding = config.hasKey("saltEncoding") ? config.getString("saltEncoding") : "utf8";
final byte[] saltBytes = ("hex".equalsIgnoreCase(saltEncoding)) ? hexStringToByteArray(salt) : salt.getBytes("UTF-8");

Integer iterations = config.hasKey("iterations") ? new Integer(config.getInt("iterations")) : new Integer(2);
Integer memory = config.hasKey("memory") ? new Integer(config.getInt("memory")) : new Integer(32 * 1024);
Integer parallelism = config.hasKey("parallelism") ? new Integer(config.getInt("parallelism")) : new Integer(1);
Integer hashLength = config.hasKey("hashLength") ? new Integer(config.getInt("hashLength")) : new Integer(32);
Argon2Mode mode = config.hasKey("mode") ? getArgon2Mode(config.getString("mode")) : Argon2Mode.ARGON2_ID;

final Argon2Kt argon2Kt = new Argon2Kt();

final Argon2KtResult hashResult = argon2Kt.hash(
mode,
passwordBytes,
saltBytes,
iterations,
memory,
parallelism,
hashLength);
final String rawHash = hashResult.rawHashAsHexadecimal(false);
final String encodedHash = hashResult.encodedOutputAsString();

WritableMap resultMap = new WritableNativeMap();
resultMap.putString("rawHash", rawHash);
resultMap.putString("encodedHash", encodedHash);

promise.resolve(resultMap);
} catch (Exception exception) {
promise.reject("Failed to generate argon2 hash", exception);
}
return result;
}

@ReactMethod
public void argon2(String password, String salt, ReadableMap config,
Promise promise) {
try {
final byte[] passwordBytes = password.getBytes("UTF-8");

String saltEncoding = config.hasKey("saltEncoding")
? config.getString("saltEncoding")
: "utf8";
final byte[] saltBytes = ("hex".equalsIgnoreCase(saltEncoding))
? hexStringToByteArray(salt)
: salt.getBytes("UTF-8");

Integer iterations = config.hasKey("iterations")
? new Integer(config.getInt("iterations"))
: new Integer(2);
Integer memory = config.hasKey("memory")
? new Integer(config.getInt("memory"))
: new Integer(32 * 1024);
Integer parallelism = config.hasKey("parallelism")
? new Integer(config.getInt("parallelism"))
: new Integer(1);
Integer hashLength = config.hasKey("hashLength")
? new Integer(config.getInt("hashLength"))
: new Integer(32);
Argon2Mode mode = config.hasKey("mode")
? getArgon2Mode(config.getString("mode"))
: Argon2Mode.ARGON2_ID;

final Argon2Kt argon2Kt = new Argon2Kt();

final Argon2KtResult hashResult =
argon2Kt.hash(mode, passwordBytes, saltBytes, iterations, memory,
parallelism, hashLength);
final String rawHash = hashResult.rawHashAsHexadecimal(false);
final String encodedHash = hashResult.encodedOutputAsString();

WritableMap resultMap = new WritableNativeMap();
resultMap.putString("rawHash", rawHash);
resultMap.putString("encodedHash", encodedHash);

promise.resolve(resultMap);
} catch (Exception exception) {
promise.reject("Failed to generate argon2 hash", exception);
}
}

public Argon2Mode getArgon2Mode(String mode) {
Argon2Mode selectedMode;
switch (mode) {
case "argon2d":
selectedMode = Argon2Mode.ARGON2_D;
break;
case "argon2i":
selectedMode = Argon2Mode.ARGON2_I;
break;
case "argon2id":
selectedMode = Argon2Mode.ARGON2_ID;
break;
default:
selectedMode = Argon2Mode.ARGON2_ID;
break;
}
@ReactMethod
public void verify(String password, String encodedHash,
Promise promise) {
try {
final byte[] passwordBytes = password.getBytes("UTF-8");
final Argon2Kt argon2Kt = new Argon2Kt();
// Auto-detect mode from the encoded hash prefix ($argon2id$, $argon2i$, $argon2d$),
// mirroring how Argon2Swift.verifyHashString works on iOS.
final Argon2Mode hashMode = detectArgon2ModeFromHash(encodedHash);
final boolean isValid =
argon2Kt.verify(hashMode, encodedHash, passwordBytes);
promise.resolve(isValid);
} catch (Exception exception) {
promise.reject("Failed to verify argon2 hash", exception);
}
}

return selectedMode;
private Argon2Mode detectArgon2ModeFromHash(String encodedHash) {
if (encodedHash == null) return Argon2Mode.ARGON2_ID;
if (encodedHash.startsWith("$argon2d$")) return Argon2Mode.ARGON2_D;
if (encodedHash.startsWith("$argon2i$")) return Argon2Mode.ARGON2_I;
// $argon2id$ or unknown — default to ARGON2_ID
return Argon2Mode.ARGON2_ID;
}

public Argon2Mode getArgon2Mode(String mode) {
Argon2Mode selectedMode;
switch (mode) {
case "argon2d":
selectedMode = Argon2Mode.ARGON2_D;
break;
case "argon2i":
selectedMode = Argon2Mode.ARGON2_I;
break;
case "argon2id":
selectedMode = Argon2Mode.ARGON2_ID;
break;
default:
selectedMode = Argon2Mode.ARGON2_ID;
break;
}

return selectedMode;
}
}
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,11 @@ declare function Argon2(
options?: Argon2Options
): Promise<Argon2Result>;

declare function verify(
password: string,
encodedHash: string,
): Promise<boolean>;

export default Argon2;

export { verify };
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ import { NativeModules } from 'react-native';
const RNArgon2Module = NativeModules.RNArgon2;

export default RNArgon2Module.argon2;
export const verify = RNArgon2Module.verify;
17 changes: 16 additions & 1 deletion ios/RNArgon2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class RNArgon2: NSObject {

@objc
func argon2(_ password: String, salt: String, config: NSDictionary? = nil, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) -> Void {
let configDict = config as! Dictionary<String,Any>
let configDict = (config as? Dictionary<String, Any>) ?? [:]

let iterations = configDict["iterations", default: 2 ] as! Int
let memory = configDict["memory", default: 32 * 1024 ] as! Int
Expand Down Expand Up @@ -60,6 +60,21 @@ class RNArgon2: NSObject {
reject("E_ARGON2", "Failed to generate argon2 hash", nsError)
}
}

@objc
func verify(_ password: String, encodedHash: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) -> Void {
do {
// Verify the password against the encoded hash
let isValid = try Argon2Swift.verifyHashString(
password: password,
hash: encodedHash
)
resolve(isValid)
} catch {
let nsError = NSError(domain: "com.poowf.argon2", code: 300, userInfo: ["Error reason": "Failed to verify argon2 hash: \(error.localizedDescription)"])
reject("E_ARGON2", "Failed to verify argon2 hash", nsError)
}
}

func getArgon2Mode(mode: String) -> Argon2Type {
switch mode {
Expand Down
11 changes: 10 additions & 1 deletion ios/RNArgon2Bridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

@interface RCT_EXTERN_MODULE(RNArgon2, NSObject)

RCT_EXTERN_METHOD(argon2: (NSString *)password salt:(NSString *)salt config:(NSDictionary *)config resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(argon2:(NSString *)password
salt:(NSString *)salt
config:(NSDictionary *)config
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(verify:(NSString *)password
encodedHash:(NSString *)encodedHash
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

@end
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
}
],
"license": "MIT",
"peerDependencies": {
"react-native": "*"
},
"devDependencies": {
"standard-version": "^9.5.0"
},
Expand Down
Loading