nodejs / readable-stream

Node-core streams for userland
https://nodejs.org/api/stream.html
Other
1.03k stars 225 forks source link

CryptoJS progresive cipher streams fail to decrypt and not sure why #495

Closed andresmejia3 closed 1 year ago

andresmejia3 commented 1 year ago

I made 2 streams, an encryptor stream and a decryptor stream using CryptoJS's progressive ciphers. I'm using streams/pipeline from readable-stream as this is being run on React Native so it has to be Browserfied.

I keep getting the result in Base64. An example of the output I get is (of course it changes every time due to a random salt/iv): Result: zvkpfuiRtVPzkbL89aR/Xw==

I'm not sure where I'm going wrong here. I'd appreciate any help, thank you so much.

const decryptionTest = () => {
  const password = 'password123'

  const source = new PassThrough()
  const destination = new PassThrough()
  const { encryptor, salt, iv } = createEncryptor(password, 'base64')
  const decryptStream = createDecryptor(password, salt, iv, 'base64')

  destination.on('data', (chunk: Buffer) => console.log('Result:', chunk.toString()))

  pipeline(source, encryptor, decryptStream, destination, (error: any) => {
    if (error)
      console.error('Pipeline Error:', error)
  })

  source.write('Hello World')
}

const createEncryptor = (password: string, encoding: 'utf8' | 'base64'): EncryptorParams => {
  const hashedPassword = SHA256(password)
  const salt = WordArray.random(64) // Create 64 byte salt
  const iv = WordArray.random(16) // Create 16 byte iv

  // Generate key of size 256 bits (keySize is the size the key should be in 32 bit Words)
  const key = PBKDF2(hashedPassword, salt, { keySize: 512 / 32, iterations: 1000 })
  const encryptor = algo.AES.createEncryptor(key, { iv })
  const encoder = encoding === 'base64' ? Base64 : Utf8

  const encryptorStream = new Transform({
    transform(chunk: Buffer, enc, callback) {
      const encodedChunk = chunk.toString(encoding)
      const encryptedChunk = encryptor.process(encodedChunk).toString(encoder)
      this.push(encryptedChunk)
      callback()
    }
  })

  return { encryptor: encryptorStream, salt, iv }
}

const createDecryptor = (password: string, salt: WordArray, iv: WordArray, encoding: 'utf8' | 'base64'): Transform => {
  const hashedPassword = SHA256(password)
  const key = PBKDF2(hashedPassword, salt, { keySize: 512 / 32, iterations: 1000 })
  const decryptor = algo.AES.createDecryptor(key, { iv })
  const encoder = encoding === 'base64' ? Base64 : Utf8

  return new Transform({
    transform(chunk: Buffer, enc, callback) {
      const encodedChunk = chunk.toString(encoding)
      const decryptedChunk = decryptor.process(encodedChunk).toString(encoder)
      this.push(decryptedChunk)
      callback()
    }
  })
}

I've been tinkering with it for awhile so I tried different orders of encodings, using WordArray.create() instead of const encodedChunk = chunk.toString(encoding), but most things I did either led to a UTF8 malform error or the result of the decryption being an empty string after calling toString().

mcollina commented 1 year ago

Thanks for reporting!

Can you provide steps to reproduce? We often need a reproducible example, e.g. some code that allows someone else to recreate your problem by just copying and pasting it. If it involves more than a couple of different file, create a new repository on GitHub and add a link to that.

andresmejia3 commented 1 year ago

Thanks for reporting!

Can you provide steps to reproduce? We often need a reproducible example, e.g. some code that allows someone else to recreate your problem by just copying and pasting it. If it involves more than a couple of different file, create a new repository on GitHub and add a link to that.

I believe the code I posted above should reproduce it. The only thing I'm missing from it is the import statements. This is the code with the import statements added.


import { algo, PBKDF2, SHA256 } from 'crypto-js'
import WordArray from 'crypto-js/lib-typedarrays'
import Base64 from 'crypto-js/enc-base64'
import Utf8 from 'crypto-js/enc-utf8'
// @ts-ignore
import { PassThrough, Transform, pipeline, Stream } from 'readable-stream'
// @ts-ignore
import { createGzip, createGunzip } from 'browserify-zlib'
import * as Buffer from 'buffer'
import RNBlob, { RNFetchBlobReadStream } from 'rn-fetch-blob'

const decryptionTest = () => {
  const password = 'password123'

  const source = new PassThrough()
  const destination = new PassThrough()
  const { encryptor, salt, iv } = createEncryptor(password, 'base64')
  const decryptStream = createDecryptor(password, salt, iv, 'base64')

  destination.on('data', (chunk: Buffer) => console.log('Result:', chunk.toString()))

  pipeline(source, encryptor, decryptStream, destination, (error: any) => {
    if (error)
      console.error('Pipeline Error:', error)
  })

  source.write('Hello World')
}

const createEncryptor = (password: string, encoding: 'utf8' | 'base64'): EncryptorParams => {
  const hashedPassword = SHA256(password)
  const salt = WordArray.random(64) // Create 64 byte salt
  const iv = WordArray.random(16) // Create 16 byte iv

  // Generate key of size 256 bits (keySize is the size the key should be in 32 bit Words)
  const key = PBKDF2(hashedPassword, salt, { keySize: 512 / 32, iterations: 1000 })
  const encryptor = algo.AES.createEncryptor(key, { iv })
  const encoder = encoding === 'base64' ? Base64 : Utf8

  const encryptorStream = new Transform({
    transform(chunk: Buffer, enc, callback) {
      const encodedChunk = chunk.toString(encoding)
      const encryptedChunk = encryptor.process(encodedChunk).toString(encoder)
      this.push(encryptedChunk)
      callback()
    }
  })

  return { encryptor: encryptorStream, salt, iv }
}

const createDecryptor = (password: string, salt: WordArray, iv: WordArray, encoding: 'utf8' | 'base64'): Transform => {
  const hashedPassword = SHA256(password)
  const key = PBKDF2(hashedPassword, salt, { keySize: 512 / 32, iterations: 1000 })
  const decryptor = algo.AES.createDecryptor(key, { iv })
  const encoder = encoding === 'base64' ? Base64 : Utf8

  return new Transform({
    transform(chunk: Buffer, enc, callback) {
      const encodedChunk = chunk.toString(encoding)
      const decryptedChunk = decryptor.process(encodedChunk).toString(encoder)
      this.push(decryptedChunk)
      callback()
    }
  })
}
mcollina commented 1 year ago

Just to clarify: are you running this on React Native?

andresmejia3 commented 1 year ago

Just to clarify: are you running this on React Native?

Yes on React Native 0.70.1

andresmejia3 commented 1 year ago

Figured it out