Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interoperability with javax.crypto #63

Closed
grant-arqit opened this issue Feb 29, 2024 · 7 comments
Closed

Interoperability with javax.crypto #63

grant-arqit opened this issue Feb 29, 2024 · 7 comments
Assignees

Comments

@grant-arqit
Copy link

I have a service which encrypts data in a kotlin appliction with the following function.

import java.nio.ByteBuffer
import java.security.SecureRandom
import java.util.*
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec

fun main() {
    val encodedKey = "U5yzshQRpa1aplMWK/QvbUXlHQyyVk+Zwxz8fyLKRLQ="
    val keyBytes = Base64.getDecoder().decode(encodedKey)
    val key = SecretKeySpec(keyBytes, "AES")

    val secret = "MySecret"
    val secretBytes = secret.toByteArray()
    val encryptedSecret = encrypt(secretBytes, key)
    println("Encrypted/Encoded string: ${Base64.getEncoder().encodeToString(encryptedSecret)}")

}

fun encrypt(plainText: ByteArray, key: SecretKey): ByteArray {
    val iv = ByteArray(12)
    SecureRandom().nextBytes(iv)
    val cipherInstance = Cipher.getInstance("AES/GCM/NoPadding")
    val gcmParameterSpec = GCMParameterSpec(16 * 8, iv)
    cipherInstance.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec)
    val encrypted = cipherInstance.doFinal(plainText)

    val byteBuffer = ByteBuffer.allocate(iv.count() + encrypted.count())
    byteBuffer.put(iv)
    byteBuffer.put(encrypted)
    return byteBuffer.array()
}
}

The console output of the above standalone code is an encrypted, base64 encoded string. One example of the encrypted string is 0L5oGCBu1Jbe/TTcMu+4wVbK32vRK7UptQjC1zedbpL6Zq3j (each execution will always be different because of the iv prefix element based on a random value)

I've fed the encoded/encrypted string into the K6 test below and I get an OperationError when calling crypto.subtle.decrypt (the import function is fine).

 // using "buffer": "^6.0.3" package
import { crypto } from 'k6/experimental/webcrypto'
import { Buffer as IsomorphicBuffer } from 'buffer/'

class Buffer extends IsomorphicBuffer {}

const base64Decode = (base64String: string): Uint8Array => {
  return Uint8Array.from(Buffer.from(base64String, 'base64'))
}

const byteArrayToString = (byteArray: Uint8Array): string => {
  return Buffer.from(byteArray).toString('utf8')
}

const decryptWithPreshared = async (encodedKey: string, encryptedEncodedData: string ): Promise<string> => {
  const encryptedData = base64Decode(encryptedEncodedData)
  const key = base64Decode(encodedKey)
  const iv = encryptedData.subarray(0, 12)
  const dataToDecrypt = encryptedData.subarray(12)
  const cryptoKey = await crypto.subtle.importKey('raw', key, "AES-GCM", false,  ['decrypt'])
  const cipher: ArrayBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, cryptoKey, dataToDecrypt)
  return byteArrayToString(new Uint8Array(cipher))
}

export const options = {
  iterations: 1
};

export default async () => {
  console.log(`Decrypted string: ${await decryptWithPreshared('U5yzshQRpa1aplMWK/QvbUXlHQyyVk+Zwxz8fyLKRLQ=', '0L5oGCBu1Jbe/TTcMu+4wVbK32vRK7UptQjC1zedbpL6Zq3j')}`)
  // OperationError thrown when executing above statement
}

Are you able advise what may be going wrong here and if there are any workarounds for a K6 test? Are there any known interoperability issues either between javax.crypto libraries and k6 webcrypto? The buffer npm libs used in the decode and byteArray to string routines successfully execute in the test, but could the conversion / decoded output be incompatible when supplying the output of these functions as inputs to k6 webcrypto? Incidently when decrypting this in a nodejs runtime environment with the isomorphic-webcrypto implementation, decryption works fine without error.

@grant-arqit
Copy link
Author

Here's a working version using standard node webcrypto

import { Buffer as IsomorphicBuffer } from 'buffer/'
const { subtle } = globalThis.crypto
class Buffer extends IsomorphicBuffer {}

const base64Decode = (base64String: string): Uint8Array => {
  return Uint8Array.from(Buffer.from(base64String, 'base64'))
}

const byteArrayToString = (byteArray: Uint8Array): string => {
  return Buffer.from(byteArray).toString('utf8')
}

const decryptWithPreshared = async (encodedKey: string, encryptedEncodedData: string ): Promise<string> => {
  const encryptedData = base64Decode(encryptedEncodedData)
  const key = base64Decode(encodedKey)
  const iv = encryptedData.subarray(0, 12)
  const dataToDecrypt = encryptedData.subarray(12)
  const cryptoKey = await subtle.importKey('raw', key, "AES-GCM", false,  ['decrypt'])
  const cipher: ArrayBuffer = await subtle.decrypt({ name: "AES-GCM", iv }, cryptoKey, dataToDecrypt)
  return byteArrayToString(new Uint8Array(cipher))
}

decryptWithPreshared('U5yzshQRpa1aplMWK/QvbUXlHQyyVk+Zwxz8fyLKRLQ=', '0L5oGCBu1Jbe/TTcMu+4wVbK32vRK7UptQjC1zedbpL6Zq3j')
  .then((txt) => {
    console.log(`Decrypted string: ${txt}`)
  })

@olegbespalov
Copy link
Contributor

Hey @grant-arqit !

I checked the case, and there may be a mismatch in types or behavior in k6 vs. nodejs. I need to check deeply what exactly, but currently, it's that in the k6, you have to:

  • use a dedicated 'k6/encoding' module for base64 decoding
  • wrap subarray into Uint8Array

🤔

For instance, below, I did some minimal working version using the data that you've provided:

import { crypto } from "k6/experimental/webcrypto";
import encoding from 'k6/encoding';


const displayArrayBufferContent = (buffer) => {
   return [...new Uint8Array(buffer)]
     .map((x) => x.toString(10))
     .join(" ");
 }

 function arrayBufferToString(buffer) {
   return String.fromCharCode.apply(null, new Uint8Array(buffer));
 }

 const base64Decode = (base64String) => {
   return new Uint8Array(encoding.b64decode(base64String));
 }

 export default async function() {
   const keyData = base64Decode('U5yzshQRpa1aplMWK/QvbUXlHQyyVk+Zwxz8fyLKRLQ=');
   const key = await crypto.subtle.importKey('raw', keyData, "AES-GCM", false,  ['decrypt']);
  
    const encryptedData = base64Decode('0L5oGCBu1Jbe/TTcMu+4wVbK32vRK7UptQjC1zedbpL6Zq3j');
    const iv = new Uint8Array(encryptedData.subarray(0, 12));
    const ciphertext = new Uint8Array(encryptedData.subarray(12));

    console.log('iv: ' + displayArrayBufferContent(iv));
    console.log('ciphertext: ' + displayArrayBufferContent(ciphertext));
  
    const plaintext = await crypto.subtle.decrypt(
      {
        name: "AES-GCM",
        iv: iv,
      },
      key,
      ciphertext,
    );

    console.log("Decrypted string: '" + arrayBufferToString(plaintext) + "'")
}

that will result me with:

INFO[0000] iv: 208 190 104 24 32 110 212 150 222 253 52 220  source=console
INFO[0000] ciphertext: 50 239 184 193 86 202 223 107 209 43 181 41 181 8 194 215 55 157 110 146 250 102 173 227  source=console
INFO[0000] Decrypted string: 'MySecret'                  source=console

Let me know if that helps
Cheers!

@olegbespalov
Copy link
Contributor

I think we also should update the k6's webcrypto docs to showcase such import 🤔

@grant-arqit
Copy link
Author

Thanks @olegbespalov , that's really useful feedback. I'll see if I can adapt our encoding libraries to use k6 encoding and see what further progress can be made.

@grant-arqit
Copy link
Author

HI @olegbespalov. I have made some progress, however I am hitting on some k6 specific encoding issues by the looks of things. Do you know how url encoding is treated?

One of my strings which includes *^ characters is encodced to %2A%5E respectively as per RFC3986, but I get the following error.

ERRO[0000] Error: GoError: illegal base64 data at input byte 40

@olegbespalov
Copy link
Contributor

olegbespalov commented Mar 14, 2024

Hey @grant-arqit !

Glad to hear that ☺️

Our encoding module is a wrapper around Golang's encoding. You have some control over which encoding is used. See the documentation at https://grafana.com/docs/k6/latest/javascript-api/k6-encoding/b64decode/ or the Golang documentation at https://pkg.go.dev/encoding/base64#pkg-variables.

So I might think that you should use something like encoding.b64decode(enc, 'url').

I'm not sure about RFC3986. Since it's about Uniform Resource Identifier (URI). Golang, base64 implementation is based on https://www.rfc-editor.org/rfc/rfc4648.html.

@grant-arqit
Copy link
Author

I've managed to resolve my crypto / encoding issues now. Thanks very much your help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants