I'm trying to build an equivalent version of SHA256 ComputeHash (from C#, the EXACT same output from the below sample), to React Native/JavaScript.
This is the following C#:
public static string Hash(string input)
{
if (string.IsNullOrWhiteSpace(input)) return "";
using (SHA256 hasher = SHA256.Create())
{
// Convert the input string to a byte array and compute the hash.
byte[] data = hasher.ComputeHash(Encoding.Unicode.GetBytes(input));
// Create a new Stringbuilder to collect the bytes
// and create a string.
StringBuilder sBuilder = new StringBuilder();
// Loop through each byte of the hashed data
// and format each one as a hexadecimal string.
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("X2"));
}
// Return the hexadecimal string.
return $"0x{sBuilder.ToString().ToLower()}";
}
}
I tried the following, but it doesn't generate the same Hash:
import * as Crypto from 'expo-crypto';
const hash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
"StringIWantToHash"
);
Would anyone know, either what's wrong with the JavaScript, or if there is an exact equivalent version of the C# one?
Expo React Native:
Solution 1:
install sha256 by
yarn add sha256
import React, { Component } from "react";
import { Text, StyleSheet, View } from "react-native";
const sha256 = require("sha256");
const isNullOrWhitespace = (input) => {
if (typeof input === "undefined" || input == null) return true;
return input.replace(/\s/g, "").length < 1;
};
const getByteArray = (input) => {
let bytes = [];
for (var i = 0, k = 0; i < input.length; i++, k += 2) {
bytes[k] = input.charCodeAt(i);
bytes[k + 1] = 0;
}
return bytes;
};
const hash = async (input) => {
if (isNullOrWhitespace(input)) {
return "";
}
var bytes = getByteArray(input);
const hashString = "0x" + sha256(bytes, { asBytes: false });
return hashString;
};
export default class App extends Component {
state = {
encodedString: "",
};
async UNSAFE_componentWillMount() {
const encodedString = await hash("test2"); //0x39a2272982dc7e6e5d109ab36ec280f6cd3b4b7440af5c739ed808d4ec02aae4
this.setState({ encodedString: encodedString });
}
render() {
const { encodedString } = this.state;
return (
<View style={styles.container}>
<Text>{encodedString}</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
});
Solution 2:
you are using ToString("X2") in c# which means you have to convert hash to HEX (base 16)
here id demo: https://snack.expo.io/#nomi9995/expo-crypto
you need to convert hash to HEX (base 16) like this
await hash.toString(16);
try this it will give the same result as c#
Code :
import React, { Component } from "react";
import { Text, StyleSheet, View } from "react-native";
import * as Crypto from "expo-crypto";
const isNullOrWhitespace = (input) => {
if (typeof input === "undefined" || input == null) return true;
return input.replace(/\s/g, "").length < 1;
};
const hash = async (input) => {
if (isNullOrWhitespace(input)) {
return "";
}
let hash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
input
);
const sBuilder = await hash.toString(16);
return `0x${sBuilder.toLowerCase()}`;
};
export default class App extends Component {
state = {
encodedString: "",
};
async UNSAFE_componentWillMount() {
const result = await hash("StringIWantToHash"); //here you can pass your string
this.setState({ encodedString: result });
}
render() {
const { encodedString } = this.state;
return (
<View style={styles.container}>
<Text>{encodedString}</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
});
Node js:
install crypto-js by
yarn add crypto-js
try this it will give the same result as c#
var CryptoJS = require("crypto-js");
const isNullOrWhitespace = (input) => {
if (typeof input === "undefined" || input == null) return true;
return input.replace(/\s/g, "").length < 1;
};
const hash = (input) => {
if (isNullOrWhitespace(input)) {
return "";
}
let hash = CryptoJS.SHA256(input);
const sBuilder=hash.toString(CryptoJS.enc.Hex);
return `0x${sBuilder.toLowerCase()}`;
};
const result=hash("StringIWantToHash");
console.log(result,"result"); // it will give the same result as C#
Solution 1: Use UTF8 instead of Unicode
Well, this is an Encoding problem.
Encoding.Unicode is Microsoft's misleading name for UTF-16 (a double-wide encoding, used in the Windows world for historical reasons but not used by anyone else). http://msdn.microsoft.com/en-us/library/system.text.encoding.unicode.aspx (see this answer)
You should be using Encoding.UTF8.GetBytes instead.
Using js-sha256 library like this:
const jssha = require('js-sha256')
function hash(input)
{
const hashString = "0x" + jssha.sha256(input)
return hashString;
}
const hashResult = hash("StringIWantToHash")
// Output: 0x29c506d0d69a16e413d63921b7de79525c43715931d8d93127dbeb46eacda2f9
We can achieve pretty similar in C# just using UTF8 encoding:
public static string Hash(string input)
{
if (string.IsNullOrWhiteSpace(input)) return "";
using (SHA256 hasher = SHA256.Create())
{
// Convert the input string to a byte array and compute the hash.
byte[] data = hasher.ComputeHash(Encoding.UTF8.GetBytes(input)); // Note that UTF8 here
// Create a new Stringbuilder to collect the bytes
// and create a string.
StringBuilder sBuilder = new StringBuilder();
// Loop through each byte of the hashed data
// and format each one as a hexadecimal string.
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("X2"));
}
// Return the hexadecimal string.
return $"0x{sBuilder.ToString().ToLower()}";
}
}
static void Main()
{
var hashResult = Hash("StringIWantToHash");
// Output: 0x29c506d0d69a16e413d63921b7de79525c43715931d8d93127dbeb46eacda2f9
}
Also, I believe that other JS/React Native libraries that help to compute SHA256 hash use UTF8 encoding too, so I think you can use any other crypto libraries for that.
Solution 2: What if you need to use Unicode?
In that case, you need to manually represent C# encoding in JS code.
While you are using Unicode encoding, after every single byte in the string goes '0' byte if it is a plain Latin character. For other symbols (with more than 255 number) Unicode required 2 bytes for that.
var input = "StringIWantToHash";
var encodedInput = Encoding.Unicode.GetBytes(input);
// Output: [83, 0, 116, 0, 114, 0, 105, 0, 110, 0, ...]
So we need to represent that in our JS code:
const jssha = require('js-sha256')
function hash(input)
{
var bytes = [];
for (var i = 0; i < input.length; i++)
{
const code = input.charCodeAt(i);
bytes = bytes.concat([code & 0xff, code / 256 >>> 0]);
}
const hashString = "0x" + jssha.sha256(bytes)
return hashString;
}
const hashResult = hash("StringIWantToHash")
// Output: 0x029dbc4b54b39bed6d684175b2d76cc5622c60fe91f0bde9865b977d0d9a531d
after two days of research it works perfectly! Two different codes give the same result.
js
const sha1 = require('sha1');
const getHash = str =>{
const hashingBytes = Buffer.from(sha1(str), "hex");
const base64Value = Buffer.from(hashingBytes).toString('base64');
return base64Value;
}
c#
System.Security.Cryptography.SHA1 sha = new System.Security.Cryptography.SHA1CryptoServiceProvider();
byte[] bytes = System.Text.Encoding.ASCII.GetBytes(str);
byte[] hashingbytes = sha.ComputeHash(bytes);
var hash = Convert.ToBase64String(hashingbytes);
Related
Our application has a group chat feature which involves end-to-end encryption of group chat messages.
The group is hosted at server.
All the encryption and decryption is handled on the clientside.
It also utilizes the same mechanism of encryption for the real-time messages.
The group chat works by foremost someone creating this group at serverside, when a client is instating its creation they generate a new key [clientside], and validate it on their end [successful encryption and decryption of a same static text from SubtleCrypto], and then successfully hosts it by sending the encrypted buffer to server where groups are managed, storing it, and then others can join if they know the secret key. We use the same AES-GCM Symmetric Key / Secret Key that is generated for this purpose. The server-side doesn't have the key stored anywhere.
Now, to validate whether the key a client trying to join this group is valid or not before joining is, with the key that was shared to them [by other means, such as email etc], at client-side, encrypt the SAME static text, and send its buffer. Then the buffer value stored at the server-side on creation time is compared to this new client joining with the buffer of the newly encrypted static text they performed on client-side, and if the buffer is equal on server-side, they are authorized into this group.
Now with reference to my previous question(s), I'm attempting to replace Web API SubtleCrypto to SJCL, and the newly generated SJCL encrypted buffer is never the same compared to the SubtleCrypto. While encrypting and decrypting between each other is already established, the problem at hand is that their buffers don't match, even though they're both using the same key, IV, and AES-GCM mode. And they both have to simultaneously work for backwards compatibility of different client versions.
Here is an example:
const buf2hex = (buffer) =>
{
return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}
const string2hex = (input) =>
{
let hex;
let result = "";
let i = 0;
for (i = 0; i < input.length; i++)
{
hex = input.charCodeAt(i).toString(16);
result += ("000" + hex).slice(-4);
}
return result
}
const hex2bytes = (string) =>
{
const normal = string.length % 2 ? "0" + string : string; // Make even length
const bytes = new Uint8Array(normal.length / 2);
for (let index = 0; index < bytes.length; ++index)
{
const c1 = normal.charCodeAt(index * 2);
const c2 = normal.charCodeAt(index * 2 + 1);
const n1 = c1 - (c1 < 58 ? 48 : 87);
const n2 = c2 - (c2 < 58 ? 48 : 87);
bytes[index] = n1 * 16 + n2;
}
return bytes;
}
//JWK K Value
const generateKey = async () =>
{
const key = await window.crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 128
}, true, ["encrypt", "decrypt"]);
const key_exported = await window.crypto.subtle.exportKey("jwk", key);
return key_exported.k;
}
//CryptoKey generated from SubtleCrypto:
const generateSubtleCryptoKey = async (kvalue) =>
{
return window.crypto.subtle.importKey(
"jwk",
{
k: kvalue,
alg: "A128GCM",
ext: true,
key_ops: ["encrypt", "decrypt"],
kty: "oct",
},
{
name: "AES-GCM",
length: 128
},
false,
["encrypt", "decrypt"]
);
}
//Cipher generated from SJCL:
const generateCipherSJCL = (kkey) =>
{
const ekkeyB64 = kkey.replace(/-/g, '+').replace(/_/g, '/'); // Base64url -> Base64 (ignore optional padding)
const ebkey = sjcl.codec.base64.toBits(ekkeyB64); // conert to bitArray
const ecipher = new sjcl.cipher.aes(ebkey);
return ecipher;
}
const encryptText = "STATIC TEXT";
const compareBuffers = async () =>
{
const kkey = await generateKey();
const cryptokey = await generateSubtleCryptoKey(kkey)
const ecipher = generateCipherSJCL(kkey);
const subtleCrypto = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: new Uint8Array(12)
}, cryptokey, new TextEncoder().encode(JSON.stringify(encryptText)));
const encryptionIv = sjcl.codec.hex.toBits(buf2hex(new Uint8Array(12).buffer));
const encryptedMessageFormat = sjcl.codec.hex.toBits(string2hex(JSON.stringify(encryptText)));
const sjclEncrypted = sjcl.mode.gcm.encrypt(ecipher, encryptedMessageFormat, encryptionIv);
const originalEncryptedSJCL = Buffer.from(sjclEncrypted);
console.log({subtleCrypto});
console.log({originalEncryptedSJCL});
const e1 = Buffer.from(new Uint8Array(sjclEncrypted));
const e2 = Buffer.from(new Uint8Array(subtleCrypto));
console.log({e1, e2}); //{e1: Uint8Array(11), e2: Uint8Array(29)}
console.log(Buffer.compare(e1, e2)); //should be 0, equal buffer.
}
compareBuffers();
I suppose I should preface this by stating that I have very limited cryptography knowledge, but why would the buffers differ when they're both encrypted and decrypted across both libraries when the mechanism is same?
Let's assume I have the following in NodeJS:
import {
gzip
} from "node-gzip";
const samlToken = fs.readFileSync(
"./localDevToken.txt",
"utf8",
);
const bufferSamlToken = Buffer.from(
samlToken.replace("\r\n", ""),
"utf8",
);
const gZipToken = await gzip(bufferSamlToken);
localDevToken = gZipToken
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "");
And I want to do the same in the frontend. How can I achieve it ?
This is what I've tried using the Pako library from https://github.com/nodeca/pako
function convertSamlToken(input) {
var samlToken = input.replace("\r\n", "");
samlToken = pako.gzip(samlToken, {
to: 'string'
});
samlToken = btoa(samlToken)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "");
return samlToken;
}
But the output is different. What is wrong ?
You are using pako incorrectly, there is no {to: 'string'} option and so the output is a Uint8Array. Here is how your function should look:
function convertSamlToken(input) {
const samlToken = input.replace("\r\n", "");
const bytes = pako.gzip(samlToken);
// Convert Uint8Array to base64 in a safe way
// See https://stackoverflow.com/a/9458996/7448536
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "");
}
I've tested this function on random UTF-8 data and it produces the exact same output except for the 10th byte since that is the OS ID. This is because node-gzip sets it to 0x0A/TOPS-20 and pako uses 0x03/Unix. If that is a problem then just add bytes[9] = 0x0a; after line 3.
My aim is for a string passed into dna will be converted using the lookup table traslateDna and returned.
For a single character this works fine but multiple characters and empty strings breaks this.
I can get it to work running it through a loop but is there a way of not requiring a loop?
Sample dna = ACGTGGT
const traslateDna = {
'G':'C',
'C':'G',
'T':'A',
'A':'U'
}
export const toRna = (dna) => {
let rnaStr = ''
for(let i =0; i < dna.length; i++){
rnaStr+=(traslateDna[dna[i]])
}
return rnaStr;
};
Below is the closest I can come, which is pretty awful. Is there a way of using the replace()method for this.
const traslateDna = {
'G':'C',
'C':'G',
'T':'A',
'A':'U'
}
export const toRna = (dna) => {
let rnaStr = '';
rnaStr = traslateDna[dna.replace()]);
}
return rnaStr;
};
Your second example is still incorrect. You should put it into codepen and try to parse it. It should not run.
To answer your question tho, You could use recursion I suppose.
const dna = 'ACGTGGT';
const traslateDna = {
'G':'C',
'C':'G',
'T':'A',
'A':'U'
}
const toRna = (dna, offset = 0) => {
if (offset == dna.length) return dna;
let rnaStr = '';
rnaStr = dna.substring(0, offset) + traslateDna[dna[offset]] + dna.substring(offset + 1, dna.length);
return toRna(rnaStr, offset + 1);
};
console.log(toRna(dna));
Although, functionally, this method and a loop are equivilant.
I am trying to transpose a c# code to a javascript using cryptojs and in the c# code it uses TripleDESCryptoServiceProvider. I can get everything exactly the values of C# in my javascript code except for the encrypting part. I get an error of "Invalid array length" This is the whole error message:
"RangeError: Invalid array length
at WordArray.init.clamp (http://localhost:8100/auth-login-login-module.js:1392:27)
at WordArray.init.concat (http://localhost:8100/auth-login-login-module.js:1357:19)
at Object.pad (http://localhost:8100/auth-login-login-module.js:652:19)
at Object._doFinalize (http://localhost:8100/auth-login-login-module.js:729:26)
at Object.finalize (http://localhost:8100/auth-login-login-module.js:400:44)
at Object.encrypt (http://localhost:8100/auth-login-login-module.js:912:41)
at Object.encrypt (http://localhost:8100/auth-login-login-module.js:438:59)
at AuthService.encryptText (http://localhost:8100/auth-login-login-module.js:6745:83)
at LoginPage.ngOnInit (http://localhost:8100/auth-login-login-module.js:6939:26)
at checkAndUpdateDirectiveInline (http://localhost:8100/vendor.js:65455:19)"
Please see my code on c# and javascript.
C#
public static string EncryptTxt(string key, string msg, CipherMode mode, Int16 KeyOffSet)
{
SHA512CryptoServiceProvider sha = new SHA512CryptoServiceProvider();
byte[] newKey = new byte[23];
using (var tdes = new TripleDESCryptoServiceProvider())
{
byte[] Results;
System.Text.UTF8Encoding UTF8 = new System.Text.UTF8Encoding();
MD5CryptoServiceProvider HashProvider = new MD5CryptoServiceProvider();
byte[] TDESKey = HashProvider.ComputeHash(UTF8.GetBytes(key));
TripleDESCryptoServiceProvider TDESAlgorithm = new TripleDESCryptoServiceProvider();
byte[] keybyte = sha.ComputeHash(Encoding.UTF8.GetBytes(key));
byte[] newKeyx = new byte[24];
Array.Copy(keybyte, KeyOffSet, newKeyx, 0, newKeyx.Length);
TDESAlgorithm.Key = newKeyx;
TDESAlgorithm.Mode = mode;
TDESAlgorithm.Padding = PaddingMode.PKCS7;
byte[] DataToEncrypt = UTF8.GetBytes(msg);
try
{
ICryptoTransform Encryptor = TDESAlgorithm.CreateEncryptor();
Results = Encryptor.TransformFinalBlock(DataToEncrypt, 0, DataToEncrypt.Length);
}
finally
{
TDESAlgorithm.Clear();
HashProvider.Clear();
}
return Convert.ToBase64String(Results);
}
javascript
encryptText = () => {
debugger;
const msg = 'xxx:juan:201910181809:12345678';
let key = crypto.enc.Utf8.parse('xxx');
key = crypto.MD5(key);
key.words.push(key.words[0], key.words[1]);
const iv = crypto.enc.Utf8.parse('xxx');
// MD5CryptoServiceProvider
const hashProvider = crypto.MD5(iv);
const TDESKey = this.wordArrayToByteArray(hashProvider, 8);
const keybyte = this.wordArrayToByteArray(crypto.SHA512(iv), 16);
const newKeyx = new Uint8Array(24);
const newkeybyte = keybyte.slice(10, 34);
Object.assign(newKeyx, newkeybyte);
const TDESAlgorithmKey = newkeybyte;
const DataToEncrypt = this.wordArrayToByteArray(crypto.enc.Utf8.parse(msg), 40);
const dteLength = DataToEncrypt.length;
const encrypted = crypto.TripleDES.encrypt(DataToEncrypt, key, {
keySize: dteLength,
mode: crypto.mode.ECB,
padding: crypto.pad.Pkcs7,
algo: TDESAlgorithmKey
});
const result = this.wordArrayToByteArray(encrypted.ciphertext, dteLength);
console.log(encrypted);
return encrypted;
}
wordToByteArray(word: any, length: any) {
const ba = [], xFF = 0xFF;
if (length > 0) {
// tslint:disable-next-line:no-bitwise
ba.push(word >>> 24);
}
if (length > 1) {
// tslint:disable-next-line:no-bitwise
ba.push((word >>> 16) & xFF);
}
if (length > 2) {
// tslint:disable-next-line:no-bitwise
ba.push((word >>> 8) & xFF);
}
if (length > 3) {
// tslint:disable-next-line:no-bitwise
ba.push(word & xFF);
}
return ba;
}
Can you please show me how to do this right. I really appreciate it!
This error is triggered when you pass an object (other data type rather than a string).
Convert your parameter to string before passing it ahead.
Try using JSON.stringify(), let me know if it works ..
The examples I've seen show essentially this:
fetch('simple.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, {})
).then(result =>
result.instance.exports...
)
But I would like to do it without making that extra HTTP request. Wondering if the only way is this (or some variation of it, which would be helpful to know):
var binary = '...mywasmbinary...'
var buffer = new ArrayBuffer(binary.length)
var view = new DataView(buffer)
for (var i = 0, n = binary.length; i < n; i++) {
var x = binary[i]
view.setInt8(i * 8, x)
}
Wondering if I have to worry about endianess or anything like that.
Or perhaps doing something with URL and blobs might be better, I'm not sure.
Yes, you are correct, in order to inline wasm modules and avoid the HTTP request, you'll have to perform some sort of encoding. I'd recommend using Base64 encoded strings as they are the most compact form.
You can encode as follows:
const readFileSync = require('fs').readFileSync;
const wasmCode = readFileSync(id);
const encoded = Buffer.from(wasmCode, 'binary').toString('base64');
You can then load the module as follows:
var encoded = "... contents of encoded from above ...";
function asciiToBinary(str) {
if (typeof atob === 'function') {
// this works in the browser
return atob(str)
} else {
// this works in node
return new Buffer(str, 'base64').toString('binary');
}
}
function decode(encoded) {
var binaryString = asciiToBinary(encoded);
var bytes = new Uint8Array(binaryString.length);
for (var i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
var module = WebAssembly.instantiate(decode(encoded), {});