diff --git a/android/src/main/cpp/sodium-jni.c b/android/src/main/cpp/sodium-jni.c index 2955126..750a722 100644 --- a/android/src/main/cpp/sodium-jni.c +++ b/android/src/main/cpp/sodium-jni.c @@ -1,4 +1,3 @@ - #include #include "sodium.h" @@ -7,6 +6,22 @@ extern "C" { #endif +/* ***************************************************************************** + * Helpers + * ***************************************************************************** + */ + +// use only for copying values, returning values here from crypto won't work +unsigned char* as_unsigned_char_array(JNIEnv *jenv, jbyteArray array) { + if (array == NULL) { + return NULL; + } + int len = (*jenv)->GetArrayLength (jenv, array); + unsigned char* buf = malloc(len); + (*jenv)->GetByteArrayRegion (jenv, array, 0, len, (jbyte*)(buf)); + return buf; +} + /* ***************************************************************************** * Sodium-specific functions * ***************************************************************************** @@ -455,6 +470,98 @@ JNIEXPORT jint JNICALL Java_org_libsodium_jni_SodiumJNI_crypto_1sign_1ed25519_1s return (jint)result; } +JNIEXPORT jint JNICALL +Java_org_libsodium_jni_SodiumJNI_base64_1variant_1ORIGINAL(JNIEnv *env, jclass clazz) { + return (jint)sodium_base64_VARIANT_ORIGINAL; +} + +JNIEXPORT jint JNICALL +Java_org_libsodium_jni_SodiumJNI_base64_1variant_1VARIANT_1ORIGINAL_1NO_1PADDING(JNIEnv *env, + jclass clazz) { + return (jint)sodium_base64_VARIANT_ORIGINAL_NO_PADDING; +} + +JNIEXPORT jint JNICALL +Java_org_libsodium_jni_SodiumJNI_base64_1variant_1VARIANT_1URLSAFE(JNIEnv *env, jclass clazz) { + return (jint)sodium_base64_VARIANT_URLSAFE; +} + +JNIEXPORT jint JNICALL +Java_org_libsodium_jni_SodiumJNI_base64_1variant_1VARIANT_1URLSAFE_1NO_1PADDING(JNIEnv *env, + jclass clazz) { + return (jint)sodium_base64_VARIANT_URLSAFE_NO_PADDING; +} + +JNIEXPORT jint JNICALL +Java_org_libsodium_jni_SodiumJNI_sodium_1base642bin(JNIEnv *jenv, jclass clazz, jbyteArray j_bin, + jint j_bin_maxlen, jbyteArray j_b64, jint j_b64_len, + jbyteArray j_ignore, jintArray j_bin_len, + jbyteArray j_b64_end, jint j_variant) { + unsigned char *bin = (unsigned char *) (*jenv)->GetByteArrayElements(jenv, j_bin, 0); + jint *len = (*jenv)->GetIntArrayElements(jenv, j_bin_len, 0); + unsigned char *b64 = as_unsigned_char_array(jenv, j_b64); + unsigned char *ignore = as_unsigned_char_array(jenv, j_ignore); + void *memory = malloc(sizeof(int)); + int *ptr = (int *)memory; + + int result = sodium_base642bin(bin, j_bin_maxlen, b64, j_b64_len, ignore, + ptr, j_b64_end, j_variant); + (*jenv)->ReleaseByteArrayElements(jenv, j_bin, (jbyte *) bin, 0); + len[0] = *ptr; + (*jenv)->ReleaseIntArrayElements(jenv, j_bin_len, len, 0); + free(memory); + return (jint)result; +} + +JNIEXPORT jchar JNICALL +Java_org_libsodium_jni_SodiumJNI_sodium_1bin2hex(JNIEnv *jenv, jclass clazz, jbyteArray j_hex, + jint j_hex_maxlen, jbyteArray j_bin, jint j_bin_len) { + unsigned char *hex = (unsigned char *) (*jenv)->GetByteArrayElements(jenv, j_hex, 0); + unsigned char *bin = as_unsigned_char_array(jenv, j_bin); + + int result = sodium_bin2hex(hex, j_hex_maxlen, bin, j_bin_len); + (*jenv)->ReleaseByteArrayElements(jenv, j_hex, (jbyte *) hex, 0); + return (jint)result; +} + +JNIEXPORT jint JNICALL +Java_org_libsodium_jni_SodiumJNI_sodium_1hex2bin(JNIEnv *jenv, jclass clazz, jbyteArray j_bin, + jint j_bin_maxlen, jbyteArray j_hex, jint j_hex_len, + jbyteArray j_ignore, jintArray j_bin_len, + jbyteArray j_hex_end) { + unsigned char *bin = (unsigned char *) (*jenv)->GetByteArrayElements(jenv, j_bin, 0); + jint *len = (*jenv)->GetIntArrayElements(jenv, j_bin_len, 0); + unsigned char *hex = as_unsigned_char_array(jenv, j_hex); + unsigned char *ignore = as_unsigned_char_array(jenv, j_ignore); + void *memory = malloc(sizeof(int)); + int *ptr = (int *)memory; + + int result = sodium_hex2bin(bin, j_bin_maxlen, hex, j_hex_len, ignore, ptr, j_hex_end); + (*jenv)->ReleaseByteArrayElements(jenv, j_bin, (jbyte *) bin, 0); + len[0] = *ptr; + (*jenv)->ReleaseIntArrayElements(jenv, j_bin_len, len, 0); + free(memory); + return (jint)result; +} + +JNIEXPORT jchar JNICALL +Java_org_libsodium_jni_SodiumJNI_sodium_1bin2base64(JNIEnv *jenv, jclass clazz, jbyteArray j_b64, + jint j_b64_maxlen, jbyteArray j_bin, jint j_bin_len, + jint j_variant) { + unsigned char *b64 = (unsigned char *) (*jenv)->GetByteArrayElements(jenv, j_b64, 0); + unsigned char *bin = as_unsigned_char_array(jenv, j_bin); + + int result = sodium_bin2base64(b64, j_b64_maxlen, bin, j_bin_len, j_variant); + (*jenv)->ReleaseByteArrayElements(jenv, j_b64, (jbyte *) b64, 0); + return (jint)result; +} + +JNIEXPORT jint JNICALL +Java_org_libsodium_jni_SodiumJNI_sodium_1base64_1encoded_1len(JNIEnv *jenv, jclass clazz, + jint j_bin_len, jint j_variant) { + return (jint) sodium_base64_encoded_len(j_bin_len, j_variant); +} + #ifdef __cplusplus } #endif diff --git a/android/src/main/java/org/libsodium/jni/SodiumJNI.java b/android/src/main/java/org/libsodium/jni/SodiumJNI.java index 08a8ea6..fd98ba2 100644 --- a/android/src/main/java/org/libsodium/jni/SodiumJNI.java +++ b/android/src/main/java/org/libsodium/jni/SodiumJNI.java @@ -69,4 +69,15 @@ public class SodiumJNI { public final static native int crypto_sign_ed25519_pk_to_curve25519(byte[] curve25519_pk, final byte[] ed25519_pk); public final static native int crypto_sign_ed25519_sk_to_curve25519(byte[] curve25519_sk, final byte[] ed25519_sk); public final static native int crypto_sign_ed25519_sk_to_pk(byte[] sk, byte[] pk); + + public final static native int base64_variant_ORIGINAL(); + public final static native int base64_variant_VARIANT_ORIGINAL_NO_PADDING(); + public final static native int base64_variant_VARIANT_URLSAFE(); + public final static native int base64_variant_VARIANT_URLSAFE_NO_PADDING(); + + public final static native char sodium_bin2base64(byte[] b64, final int b64_maxlen, final byte[] bin, final int bin_len, final int variant); + public final static native int sodium_base642bin(final byte[] bin, int bin_maxlen, final byte[] b64, final int b64_len, final byte[] ignore, int[] bin_len, final byte[] b64_end, final int variant); + public final static native char sodium_bin2hex(byte[] hex, int hex_maxlen, byte[] bin, final int bin_len); + public final static native int sodium_hex2bin(byte[] bin, final int bin_maxlen, final byte[] hex, final int hex_len, final byte[] ignore, int[] bin_len, final byte[] hex_end); + public final static native int sodium_base64_encoded_len(final int bin_len, final int variant); } diff --git a/android/src/main/java/org/libsodium/rn/RCTSodiumModule.java b/android/src/main/java/org/libsodium/rn/RCTSodiumModule.java index 208864d..ee46d73 100644 --- a/android/src/main/java/org/libsodium/rn/RCTSodiumModule.java +++ b/android/src/main/java/org/libsodium/rn/RCTSodiumModule.java @@ -665,4 +665,127 @@ public void crypto_sign_ed25519_sk_to_pk(final String sk, final Promise p) { p.reject(ESODIUM, ERR_FAILURE, t); } } + + // *************************************************************************** + // * Utils + // *************************************************************************** + + @ReactMethod + public void to_base64(final String message, final int variant, final Promise p) { + byte[] m = message.getBytes(StandardCharsets.UTF_8); + String result = this.binToBase64(m, variant); + if (result == null) { + p.reject(ESODIUM,ERR_FAILURE); + } else { + p.resolve(result); + } + } + + @ReactMethod + public void from_base64(final String cipher, final int variant, final Promise p) { + byte[] result = this.base64ToBin(cipher, variant); + if (result == null) { + p.reject(ESODIUM,ERR_FAILURE); + } else { + p.resolve(new String(result, StandardCharsets.UTF_8)); + } + } + + @ReactMethod + public void to_hex(final String message, final Promise p) { + byte[] m = message.getBytes(StandardCharsets.UTF_8); + String result = this.binToHex(m); + if (result == null) { + p.reject(ESODIUM,ERR_FAILURE); + } else { + p.resolve(result); + } + } + + @ReactMethod + public void from_hex(final String cipher, final Promise p) { + byte[] result = this.hexToBin(cipher); + if (result == null) { + p.reject(ESODIUM,ERR_FAILURE); + } else { + p.resolve(new String(result, StandardCharsets.UTF_8)); + } + } + + private String binToBase64(final byte[] data, final int variant) { + try { + if (data.length <= 0 || variant == 0) + return null; + else { + int encoded_len = Sodium.sodium_base64_encoded_len(data.length, variant); + byte[] encoded = new byte[encoded_len]; + Sodium.sodium_bin2base64(encoded, encoded_len, data, data.length, variant); + return new String(encoded, StandardCharsets.UTF_8); + } + } + catch (Throwable t) { + return null; + } + } + + private byte[] base64ToBin(String cipher, final int variant) { + try { + byte[] c = cipher.getBytes(StandardCharsets.UTF_8); + + if (c.length <= 0 || variant == 0) + return null; + + else { + int blen = c.length; + byte[] decoded = new byte[blen]; + int[] decoded_len = new int[1]; + int result = Sodium.sodium_base642bin(decoded, blen, c, c.length, null, decoded_len, null, variant); + if (result != 0) + return null; + else + return Arrays.copyOfRange(decoded, 0, decoded_len[0]); + } + } + catch (Throwable t) { + return null; + } + } + + private String binToHex(final byte[] data) { + try { + if (data.length <= 0) + return null; + + else { + int encoded_len = data.length * 2 + 1; + byte[] encoded = new byte[encoded_len]; + Sodium.sodium_bin2hex(encoded, encoded_len, data, data.length); + return new String(encoded, StandardCharsets.UTF_8); + } + } catch (Throwable t) { + return null; + } + } + + private byte[] hexToBin(String cipher) { + try { + byte[] c = cipher.getBytes(StandardCharsets.UTF_8); + + if (c.length <= 0) + return null; + + else { + int blen = c.length / 2; + byte[] decoded = new byte[blen]; + int[] decoded_len = new int[1]; + int result = Sodium.sodium_hex2bin(decoded, blen, c, c.length, null, decoded_len, null); + if (result != 0) + return null; + else + return Arrays.copyOfRange(decoded, 0, decoded_len[0]); + } + } catch (Throwable t) { + return null; + } + } } diff --git a/index.d.ts b/index.d.ts index f4e0a8f..17de5cc 100644 --- a/index.d.ts +++ b/index.d.ts @@ -320,6 +320,18 @@ declare module "react-native-sodium" { algo: number ): Promise; + + // + // Utils + // + export function to_base64(message: string, variant: number): Promise; + + export function from_base64(cipher: string, variant: number): Promise; + + export function to_hex(message: string): Promise; + + export function from_hex(cipher: string): Promise; + /** * Bytes of salt on password hashing, the pwhash* API. */ @@ -370,4 +382,12 @@ declare module "react-native-sodium" { * Version 1.3 of the Argon2id algorithm, available since libsodium 1.0.13. */ export const crypto_pwhash_ALG_ARGON2ID13: number; + + export const base64_variant_ORIGINAL: number; + + export const base64_variant_VARIANT_ORIGINAL_NO_PADDING: number; + + export const base64_variant_VARIANT_URLSAFE: number; + + export const base64_variant_VARIANT_URLSAFE_NO_PADDING: number; } diff --git a/ios/RCTSodium/RCTSodium.h b/ios/RCTSodium/RCTSodium.h index 6929ff9..ec3b2ae 100644 --- a/ios/RCTSodium/RCTSodium.h +++ b/ios/RCTSodium/RCTSodium.h @@ -44,4 +44,10 @@ - (void) crypto_box_seal:(NSString*)m pk:(NSString*)pk resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; - (void) crypto_box_seal_open:(NSString*)c pk:(NSString*)pk sk:(NSString*)sk resolve: (RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; +- (void) to_base64:(NSString*)message variant:(NSNumber*)variant resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; +- (void) from_base64:(NSString*)cipher variant:(NSNumber*)variant resolve: (RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; + +- (void) to_hex:(NSString*)message resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; +- (void) from_hex:(NSString*)cipher resolve: (RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; + @end diff --git a/ios/RCTSodium/RCTSodium.m b/ios/RCTSodium/RCTSodium.m index c785031..115095c 100644 --- a/ios/RCTSodium/RCTSodium.m +++ b/ios/RCTSodium/RCTSodium.m @@ -487,4 +487,126 @@ + (BOOL)requiresMainQueueSetup } } +// ***************************************************************************** +// * Utils +// ***************************************************************************** +RCT_EXPORT_METHOD(to_base64:(NSString*)message variant:(NSNumber * _Nonnull)variant resolve: (RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +{ + NSData *m = [message dataUsingEncoding:NSUTF8StringEncoding]; + + if (m == nil || !variant) { + reject(ESODIUM, ERR_FAILURE, nil); + } else { + NSString *encodedString = [self binToBase64:m variant:variant]; + if (encodedString == nil) + reject(ESODIUM, ERR_FAILURE, nil); + else { + resolve(encodedString); + } + } +} + +RCT_EXPORT_METHOD(from_base64:(NSString*)cipher variant:(NSNumber * _Nonnull)variant resolve: (RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +{ + if (!cipher || !variant) { + reject(ESODIUM, ERR_FAILURE, nil); + } else { + NSData *result = [self base64ToBin:cipher variant:variant]; + if (result == nil) + reject(ESODIUM, ERR_FAILURE, nil); + else { + NSString *decodedString = [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding]; + resolve(decodedString); + } + } +} + +RCT_EXPORT_METHOD(to_hex:(NSString*)message resolve: (RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +{ + NSData *m = [message dataUsingEncoding:NSUTF8StringEncoding]; + NSString *result = [self binToHex:m]; + if (result == nil) reject(ESODIUM, ERR_FAILURE, nil); + else { + resolve(result); + } +} + +RCT_EXPORT_METHOD(from_hex:(NSString*)cipher resolve: (RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +{ + NSData *result = [self hexToBin:cipher]; + if (result == nil) reject(ESODIUM, ERR_FAILURE, nil); + else { + const NSString *res = [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding]; + resolve(res); + } +} + +- (NSString *) binToBase64:(NSData*)bin variant:(NSNumber * _Nonnull)variant { + if (!bin || !variant) return nil; + + else if (bin.length == 0) { + return nil; + } else { + const size_t max_len = sodium_base64_encoded_len(bin.length, [variant intValue]); + char * encoded = (char *) sodium_malloc(max_len); + @try { + sodium_bin2base64(encoded, max_len, [bin bytes], bin.length, [variant intValue]); + NSString *res = [NSString stringWithCString:encoded encoding:NSUTF8StringEncoding]; + return res; + } + @catch (NSException *exception) { + return nil; + } + } +} + +- (NSData * __strong) base64ToBin:(NSString*)cipher variant:(NSNumber * _Nonnull)variant { + const NSData *c = [cipher dataUsingEncoding:NSUTF8StringEncoding]; + + if (c && variant) { + + // since libsodium doesn't provide the reverse of + // sodium_base64_encoded_len(size_t bin_len, int variant) + // to estimate bin_maxlen, we set it conservatively to + // the size of the base64 representation + + size_t clen = [c length]; + unsigned char * const decoded = (unsigned char * const) sodium_malloc(clen); + size_t decoded_len = [NSNumber numberWithLongLong: clen].unsignedLongLongValue; + if (sodium_base642bin(decoded, clen, [c bytes], clen, NULL, &decoded_len, NULL, [variant intValue]) != 0) + return nil; + else { + return [NSData dataWithBytesNoCopy:decoded length:decoded_len freeWhenDone:NO]; + } + } + return nil; +} + +- (NSString * __strong) binToHex:(NSData*)bin { + size_t hex_maxlen = [bin length] * 2 + 1; + char * const encoded = (char * const) sodium_malloc(hex_maxlen); + @try { + sodium_bin2hex(encoded, hex_maxlen, [bin bytes], [bin length]); + return [NSString stringWithCString:encoded encoding:NSUTF8StringEncoding]; + } + @catch (NSException *exception) { + return nil; + } +} + +- (NSData * __strong) hexToBin:(NSString*)hex { + const NSData *h = [hex dataUsingEncoding:NSUTF8StringEncoding]; + + size_t clen = [h length]; + unsigned char * const encoded = (unsigned char * const) sodium_malloc(clen); + size_t decoded_len = [NSNumber numberWithLongLong: clen].unsignedLongLongValue; + if (sodium_hex2bin(encoded, clen, [h bytes], clen, NULL, &decoded_len, NULL) != 0) { + return nil; + } + else { + return [NSData dataWithBytesNoCopy:encoded length:decoded_len freeWhenDone:NO]; + } +} + + @end