react-native-quick-crypto icon indicating copy to clipboard operation
react-native-quick-crypto copied to clipboard

✨Support for crypto.subtle.generateKey("AES-GCM") & crypto.subtle.importKey("pkcs8"+RSA-OAEP)

Open marc-wittwer opened this issue 1 year ago • 1 comments

What feature or enhancement are you suggesting?

This repo looks great. Thanks for all the work!

I'm currently on a mission to implement our encryption functions that work in the react webapp and NodeJS in our react native app. These functions use the node:crypto and Web Crypto API.

We use the following functions from the crypto.subtle.

Operation Algorithm Supported
crypto.subtle.importKey("raw", "AES-GCM") AES-GCM
crypto.subtle.generateKey("AES-GCM") AES-GCM
crypto.subtle.exportKey("raw") AES-GCM
crypto.subtle.encrypt("AES-GCM") AES-GCM
crypto.subtle.decrypt("AES-GCM") AES-GCM
crypto.subtle.generateKey("RSA-OAEP") RSA-OAEP
crypto.subtle.importKey("spki", "RSA-OAEP") RSA-OAEP
crypto.subtle.importKey("pkcs8", "RSA-OAEP") RSA-OAEP
crypto.subtle.encrypt("RSA-OAEP") RSA-OAEP
crypto.subtle.decrypt("RSA-OAEP") RSA-OAEP
crypto.subtle.exportKey("spki", publicKey) RSA-OAEP
crypto.subtle.exportKey("pkcs8", privateKey) RSA-OAEP
crypto.subtle.importKey("raw", "PBKDF2") PBKDF2
crypto.subtle.deriveBits("PBKDF2") PBKDF2

The only two function that we are missing are:

cryptoKey = await crypto.subtle.generateKey(
        {
          name: "AES-GCM",
          length: 256,
        },
        true,
        ["encrypt", "decrypt"],
      )
await crypto.subtle.importKey(
        "pkcs8",
        arrayBuffer,
        { name: "RSA-OAEP", hash: "SHA-256" },
        true,
        ["decrypt"],
      )

What Platforms whould this feature/enhancement affect?

iOS, Android

Alternatives/Workarounds

The current workaround is using the react-native-webview-crypto which works. This brings window.crypto.subtle to your React Native application. It does this by communicating with a hidden WebView, which performs the actual computation.

However, this does not seem great.

Additional information


I saw this comment. I can try to setup a unit test that uses the two missing functions.

Unit test: `importKey("RSA-OAEP")`
import { decode, encode } from "js-base64"

const arrayBufferToBase64 = (buffer: ArrayBuffer) => {
  const bytes = new Uint8Array(buffer)
  let binary = ""
  for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i])
  }
  return btoa(binary)
}

const base64ToUint8Array = (base64: string) => {
  const binaryString = atob(base64)
  const bytes = new Uint8Array(binaryString.length)
  for (let i = 0; i < binaryString.length; i++) {
    bytes[i] = binaryString.charCodeAt(i)
  }
  return bytes
}

export const testImportKeyRSAOAEP = async () => {
  const publicKeyBase64 = `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Yxs1i4Z5HzsewxTAAhQ8hMrmlzMvQ4EL1+grkjWvhGJAFjxK9OapVQJ7yOhovt4HB8x6EY0na2JF9X/z82HcNVaO5twL1y50783WIHHoO5Z5VgK6LLF/HdhlqXqSO9LuqpSmlobv+YSLM09phpOmZ2y4IBiD08AROIW0qAmOjXZjfyqxc9I2ZQRB89Ek5VBnaimnFNto06FMei2rPLplD0Ez05Xrib44LlS4ofmbAkQsjplnNqMZHj1kaErzHqxBAWZyna9J8V3evUOwlUvSJUsyFfR869UQtgSqDhW6m/IDBQIT1PLov9nLExVkF5CJzuty4gIbW9eqxHeO7fGiwIDAQAB`
  const publicKeyUint8array = base64ToUint8Array(publicKeyBase64)
  const publicKeyArrayBuffer = publicKeyUint8array.buffer
  const encryptorKey = await crypto.subtle.importKey(
    "spki",
    publicKeyArrayBuffer,
    { name: "RSA-OAEP", hash: "SHA-256" },
    true,
    ["encrypt"],
  )

  const data = "Hello World!"
  const base64Data = encode(data) // utf8ToBase64(data)
  const dataUint8array = base64ToUint8Array(base64Data)
  const dataArrayBuffer = dataUint8array.buffer

  const encryptedDataRSA = await crypto.subtle.encrypt(
    { name: "RSA-OAEP" },
    encryptorKey,
    dataArrayBuffer,
  )

  const privateKeyBase64 = `MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDRjGzWLhnkfOx7DFMACFDyEyuaXMy9DgQvX6CuSNa+EYkAWPEr05qlVAnvI6Gi+3gcHzHoRjSdrYkX1f/PzYdw1Vo7m3AvXLnTvzdYgceg7lnlWArossX8d2GWpepI70u6qlKaWhu/5hIszT2mGk6ZnbLggGIPTwBE4hbSoCY6NdmN/KrFz0jZlBEHz0STlUGdqKacU22jToUx6Las8umUPQTPTleuJvjguVLih+ZsCRCyOmWc2oxkePWRoSvMerEEBZnKdr0nxXd69Q7CVS9IlSzIV9Hzr1RC2BKoOFbqb8gMFAhPU8ui/2csTFWQXkInO63LiAhtb16rEd47t8aLAgMBAAECggEAITsMs3aCIqrw8Z6Ftxaah5kkrAkVatHDNiQLHjhs3Z14RXbVYCbhemB2ZtcWtfr9FDCaQISJqYuwlvgX5kNovCsJcTR4OPqSeZL0WvPRzaKe3PD2Yeqf3Satci+DlOdl8gc6rEGn7um0bihqI2I+nrvUdyfE5TqZB1N3XRWKmmZQS72ZJzpK1iZgzqyFBdp4UuhFaN/V+LZXCnIaglir4Td0VIOl71vDZkRPdtmy8jupYxbCk/B3DEKWDSadZgJvWBtTNJp38K8g6C2FbEmXwYqMP6fUp2C3Dec9+rSL/eFxV24lmnHjVyZtbr+JvjgZJWkq1GbVOszbms3Kl8uyEQKBgQDtLDS5R312BCVo7Sha2NN3vvsF89ErtdjDpASb9psKyVBkU32MBPOCZRXYa+WHvATNORHuYDU0V/nVrfZ/UsGlEf2c7osRGBTB32//XcszB6OqDBJlMV2zstDL6XatjiMJ8mUNR7LlzxfC/BpyvQ2Zotnm7c0ahVCiKrMbVvKsiQKBgQDiLthAl0kT4Dc8XNR9VvX0ZmK9FHb0i4pOIlgligzvLPnXPcSqYgv1iK0RG8mdFP4jupOyaQgYAzHYr06vOAiTFnIFSPMfSS35F27A3ukDsHHffpDwVn7j3dVf+DW8HpxSrvc//Baxb0OtOU816kBXyR89bi4qrsygxjEoYzLdcwKBgBysMnePyAAjgi5MNYu+GNqqMQjIMCp7oogMZS5Bwv6r1dc7LLtnwdSqydhPOwGM3nu9AYjzApugYyjNDjbYV2bQZPu67v8TDTde/tg9i5pQux2MthCbxjs6S/nK8LkMrPm/3y2a1Grp/XJqLfxfFKzVPkinyRsCsPvZ86tDeLUZAoGBAJoJ3zs2DQ3dQKD6c7ic9cqpxAsTmeP3+Iw39aIzP5XQIqMFLSAAwDZLC9q/+vHg7ye0FIyH3XxFCLiSw9qvJZ/OxH527STceNPQspvl8/mQPC1CjEEyFx7m4D+I0ke47Suef0LzUx0qMoQRqLGGRKXEkmMK26Q0AaZo8+eWj3ijAoGAcUm27e9/TzyUmegV/4L/oMxnbLKoaedvOVEFm2fa0VuwfzNPNXtol2vZoTejJqnpEDEaEmRxN3mabapmmltVrfZes4KfGYDvjK9phSPnT/0LJNpbgu6su+SX/AB42o5pC6ckoh4fpFr8RsnU0qWvFcHtzko2l3rcfs5j7il3djU=`
  const uint8array = base64ToUint8Array(privateKeyBase64)
  const arrayBuffer = uint8array.buffer

  const key = await crypto.subtle.importKey(
    "pkcs8",
    arrayBuffer,
    { name: "RSA-OAEP", hash: "SHA-256" },
    true,
    ["decrypt"],
  )
  // expect(key).toBeDefined()
  // expect(key.type).toBe("private")
  // expect(key.algorithm.name).toBe("RSA-OAEP")
  // expect(key.usages).toContain("decrypt")

  const decryptedDataRSA = await crypto.subtle.decrypt({ name: "RSA-OAEP" }, key, encryptedDataRSA)

  // decode() is base64ToUtf8()
  const decryptedDataUTF8 = decode(arrayBufferToBase64(decryptedDataRSA))
  console.log(decryptedDataUTF8)

  // expect(decryptedDataUTF8).toBe(data)
}
Unit test: `generateKey("AES-GCM")`
import { decode, encode } from "js-base64"

const arrayBufferToBase64 = (buffer: ArrayBuffer) => {
  const bytes = new Uint8Array(buffer)
  let binary = ""
  for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i])
  }
  return btoa(binary)
}

export const testGenerateKeyAESGCM = async () => {
  const cryptoKey = await crypto.subtle.generateKey(
    {
      name: "AES-GCM",
      length: 256,
    },
    true,
    ["encrypt", "decrypt"],
  )
  expect(cryptoKey).toBeDefined()
  expect(cryptoKey.type).toBe("secret")
  expect(cryptoKey.algorithm.name).toBe("AES-GCM")

  const aesKeyBuffer = await crypto.subtle.exportKey("raw", cryptoKey)
  expect(aesKeyBuffer).toBeInstanceOf(ArrayBuffer)
  expect(aesKeyBuffer.byteLength).toBe(32) // 256 bits = 32 bytes

  const aesKeyBase64 = arrayBufferToBase64(aesKeyBuffer)
  expect(typeof aesKeyBase64).toBe("string")
  expect(aesKeyBase64.length).toBeGreaterThan(0)
}

However, regarding the actual implementation of the missing functions generateKey("AES-GCM") and importKey("pkcs8"+RSA-OAEP) I wouldn't know where to start.

Let me know how I could help getting these functions supported by react-native-quick-crypto.

marc-wittwer avatar Dec 29 '24 11:12 marc-wittwer

would also like to see crypto.subtle.importKey for pkcs8, looking through the code myself to see if I can write a PR but I'm not sure I'll have any real time to sink into this at the moment. would be great for a quick solution that doesn't require react-native-webview-crypto

arron-taylor avatar Mar 26 '25 15:03 arron-taylor