[solved] Convert Slice(UInt16) to Slice(UInt8)

What’s the most efficient way to do this? - Convert Slice(UInt16) to Slice(UInt8)

Background, I have to decrypt some data encrypted by a C# app.
The password is a string, but encoded in UTF16 as Windows.

I want to do something like this

cipher = OpenSSL::Cipher.new("aes-256-cbc")
cipher.padding = true
cipher.decrypt

key = IO::Memory.new(Bytes.new(32))
key.write "Password"
cipher.key = key.to_slice

But as the "Password" is UTF16 so technically needs to be encoded like "P\x00a\x00s\x00 etc
So then did

key = IO::Memory.new(Bytes.new(32))
key.write "Password".to_utf16
cipher.key = key.to_slice

But that complains about the Slice being the wrong size.
I assume there is a simple solution to this, something similar to

password = "Password".to_utf16
Bytes.new(password, password.bytesize)

Basically I’m stumped

Example and failure message please? Maybe it’s because #to_utf16 adds a trailing NULL ?

@rogerdpack nothing I’ve tried has compiled, just completely unsure of the optimal way to do this.
Basically I figured that a Slice(UInt16) is a pointer to some memory and it should be possible to represent that memory as Bytes given you can call #bytesize to get the size

i.e.

password = "Password".to_utf16
Bytes.new(password, password.bytesize)

results in

error in line 3
Error: no overload matches 'Slice(UInt8).new' with types Slice(UInt16), Int32

Overloads are:
 - Slice(T).new(pointer : Pointer(T), size : Int, *, read_only = false)
 - Slice(T).new(size : Int, value : T, *, read_only = false)
 - Slice(T).new(size : Int, *, read_only = false)
 - Slice(T).new(size : Int, *, read_only = false, &block)

and I need the UTF16 representation of the string in bytes to pass to cipher.key

You’re on the correct path. As the error message says, you need to pass a pointer as first argument of Slice.new. You can use password.to_unsafe to get a pointer to the slice. However, pointer must be of the result type you want to get, so you need a pointer cast from Pointer(UInt16) to Pointer(UInt8):

password = "Password".to_utf16
Bytes.new(password.to_unsafe.as(Pointer(UInt8)), password.bytesize)
1 Like

Thanks @straight-shoota!
I feel like this would be a handy addition to the standard library.

struct Slice(T)
  def to_bytes
    Bytes.new(self.to_unsafe.as(Pointer(UInt8)), self.bytesize)
  end
end

What do you think?

3 Likes

So in the end I went with this

    cipher = OpenSSL::Cipher.new("aes-256-cbc")
    cipher.padding = true
    cipher.decrypt

    # LE for little endian and avoids a byte order mark
    password = @password.encode("UTF-16LE")

    key = IO::Memory.new(Bytes.new(32))
    key.write password
    cipher.key = key.to_slice

As using the underlying bytes directly ignores endianness and we are targeting AMD64 and ARM64 platforms.

Thanks again for your help @straight-shoota and for the debate @asterite :slight_smile:

1 Like