Over the past few months I have been spending a large amount of time wondering how to encrypt data using Ruby, in InfoWorks ICM. For those who don't know, encryption refers to the process of transforming sensitive data into an unreadable form where it is unlikely to be intercepted by unauthorized viewers.
Note: Encryption does have other uses, for example, file modification prevention and software signing.
From the description above you might be able to see why encryption is so important, especially when handling sensitive client data. In hydraulic modelling this is often overlooked. However if the results or comments inside a hydraulic model became public, clients may be put in an uncomfortable position. Of course, as professionals this is something that we would like to avoid at all costs.
Typically most Ruby runtime environments have access to the OpenSSL library. OpenSSL is a robust, commercial-grade, and full-featured toolkit for data security especialyl over the internet. OpenSSL provides methods for both encrypting and decrypting data. Most encryption libraries for Ruby build off of the cryptographic services that the OpenSSL library provides.
To encrypt data with OpenSSL, you can run the following code:
require 'openssl'
cipher = OpenSSL::Cipher::AES.new(128, :CBC) #new(keylength,cipherMode)
cipher.encrypt
key = cipher.random_key #16 bytes for 128-bit keylength, 32 for 256-bit keylength
iv = cipher.random_iv #16 bytes no matter of keylength
#Now let's encrypt some data!
data = "super secure data"
encrypted = cipher.update(data) + cipher.final
p encrypted #secured data
Later we can decrypt the data like so:
decipher = OpenSSL::Cipher::AES.new(128, :CBC)
decipher.decrypt
decipher.key = key
decipher.iv = iv
#Decrypt the data
plain = decipher.update(encrypted) + decipher.final
puts plain #the decrypted text
A full example is available here.
Great! So now we know how to encrypt and decrypt data with Ruby's library OpenSSL. That's great! Or at least it would be if ICM supported OpenSSL. As of the current version of InfoWorks ICM v8, OpenSSL isn't supported by ICM's embedded version of Ruby... So. this begs the question, what can we use instead?
Simple! We use the Microsoft .NET Framework!
In a nutshell, the Microsoft .NET framework is a series of object oriented libraries specific for building applications on Windows (Note: some libraries are cross platform). You will normally be able to find common algorithms, such as ones for encryption, somewhere within the .NET framework.
A certain subset of .NET classes are exposed to COM/OLE. We can access these classes and use them to augment Ruby's functionality using the WIN32OLE class. A full list of .NET classes exposed to COM/OLE on my pc can be found here. Note: This may depend on the version of .NET installed on your machine.
To actually use a .NET class in Ruby, we can instantiate it with WIN32OLE.new
method. For example, here we instantiate the System.Text.UTF8Encoding
.NET class:
require 'win32ole'
utf8 = WIN32OLE.new("System.Text.UTF8Encoding")
puts utf8.GetByteCount_2("hello world") #_2 is the 2nd version of GetByteCount which has a single paramater (Character array) See: https://msdn.microsoft.com/en-us/library/z2s2h516(v=vs.110).aspx
#=> 11
In some cases, like in the case of using the encryption libraries, we need to use arrays of a certain data type. In the above example we had to use an array of characters, which is simply what a String
is. However this was ultimately a fluke on Ruby's behalf. In the example of the encryption libraries, we have to pass byte array In this example it is often better to use the variant datatype:
#Syntax:
#WIN32OLE_VARIANT.new([x,y,z],type)
#Example: a 3x3x3 matrix of booleans
mat1 = WIN32OLE_VARIANT.array([3,3,3],WIN32OLE::VARIANT::VT_BOOL)
#Example a 2x6 matrix of ints
mat2 = WIN32OLE_VARIANT.array([2,6],WIN32OLE::VARIANT::VT_INT)
#Example an array of 8 strings:
arr1 = WIN32OLE_VARIANT.array([8],WIN32OLE::VARIANT::VT_BSTR)
After creating the array you can fill it with data:
arr = ["a","b","c","d","e","f","g","h"]
oleArr = WIN32OLE_VARIANT.array([8],WIN32OLE::VARIANT::VT_BSTR)
arr.each_with_index { |val,ind| oleArr[ind]=val}
One of the classes provided by the .NET framework is the System.Security.Cryptography.TripleDESCryptoServiceProvider
class. This class can be instantiated with COM/OLE to encrypt data on demand. First let's look at a C#.NET example of how to use the library:
string data = "Please encyrpt me"
byte[] dataArray = UTF8Encoding.UTF8.GetBytes(data);
string key = "My amazing key?!" //Key to be a maximum of 16 bytes!
byte[] keyArray = UTF8Encoding.UTF8.GetBytes(key)
string iv = "a7bv!ad-" //Salt - always 8 bytes
byte[] ivArray = UTF8Encoding.UTF8.GetBytes(iv)
//Encrypt
TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider();
tdes.key = keyArray
tdes.iv = ivArray
byte[] aEncrypted = tdes.CreateEncryptor().TransformFinalBlock(dataArray,0,dataArray.Length)
Console.write(Convert.ToBase64String(resultArray,0,aEncrypted.Length))
//Decrypt
TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider();
tdes.key = keyArray
tdes.iv = ivArray
byte[] aDecrypted = tdes.CreateDecryptor().TransformFinalBlock(aEncrypted,0,aEncrypted.Length)
Console.Write(UTF8Encoding.UTF8.GetString(aDecrypted))
In ruby, the process of encrypting and decrypting data is basically the same, but with a few caviats. For example, I had to create a special class ByteArray
to transfer the data to in a form that .NET wouldn't complain about... But other than that, it's just the same recycled code:
require 'win32ole'
require 'base64'
class ByteArray
attr_reader :root, :length, :value
def initialize(arr)
@root = arr
@length = arr.length
vArr = WIN32OLE_VARIANT.array([arr.length],WIN32OLE::VARIANT::VT_UI1)
arr.each_with_index {|value,index| vArr[index]=value}
@value = vArr
end
end
data = "Please encyrpt me"
oData = ByteArray.new(data.each_byte.to_a)
key = "My amazing key?!" #Key to be a maximum of 16 bytes!
oKey = ByteArray.new(key.each_byte.to_a)
iv = "a7bv!ad-" #Salt - always 8 bytes
oiv = ByteArray.new(iv.each_byte.to_a)
#Encrypted
crypto = WIN32OLE.new("System.Security.Cryptography.TripleDESCryptoServiceProvider")
crypto.key = oKey.value
crypto.iv = oiv
aEncrypted = crypto.CreateEncryptor.TransformFinalBlock(oData.value,0,oData.length)
#Print encrypted data
p aEncrypted.map {|x| x.chr}.join
#Decrypt data
oEncrypted = ByteArray.new(aEncrypted)
aDecrypted = crypto.CreateDecryptor.TransformFinalBlock(oEncrypted.value,0,oEncrypted.length)
#Print decrypted data
puts aDecrypted.map {|x| x.chr}.join
Now of course the above is a bit long winded, so to make it easier I created a Crypt
class:
require 'base64'
require 'win32ole'
require 'digest'
class ByteArray
attr_reader :root, :length, :value
def initialize(arr)
@root = arr
@length = arr.length
vArr = WIN32OLE_VARIANT.array([arr.length],WIN32OLE::VARIANT::VT_UI1)
arr.each_with_index {|value,index| vArr[index]=value}
@value = vArr
end
end
#Use .NET libraries to encrypt/decrypt data
class Crypt
attr_accessor :crypto
def initialize(key,iv = "#a7B-!a@")
key = Digest::MD5.digest(key) #MD5 ensures that key is always 16 characters long
@sKey = key
@sIV = iv
@crypto = WIN32OLE.new("System.Security.Cryptography.TripleDESCryptoServiceProvider")
@crypto.key = ByteArray.new(key.each_byte.to_a).value
@crypto.iv = ByteArray.new(iv.each_byte.to_a).value
end
def encrypt(sData)
aToEncrypt = sData.each_byte.to_a
toEncrypt = ByteArray.new(aToEncrypt)
aEncrypted = @crypto.CreateEncryptor.TransformFinalBlock(toEncrypt.value, 0, toEncrypt.length)
#aEncrypted = @crypto.CreateEncryptor.TransformFinalBlock(sData, 0, sData.length + 1)
return Base64.encode64(aEncrypted)
end
def decrypt(sData)
aToDecrypt = Base64.decode64(sData).each_byte.to_a
toDecrypt = ByteArray.new(aToDecrypt)
aDecrypted = @crypto.CreateDecryptor.TransformFinalBlock(toDecrypt.value, 0, toDecrypt.length)
return aDecrypted.map {|x| x.chr}.join
end
end
Using the above class we can now easily encrypt and decrypt data:
require 'Crypt.rb'
crypt = Crypt.new("My awesome key that can be any number of characters long!")
data = crypt.encrypt("Hello world)
puts data
puts crypt.decrypt(data)
- Encrypting/Decrypting text data in a model, e.g. Comments and user text data.
- Encrypting/Decrypting files, for example simulation results/reports.
As a brief example I thought I would quickly write some code to encrypt the comments of a model. It has to be said that this algorithm is still not perfect, as each comment is encrypted individually. Therefore all comments which are the same will have the same cypher text. Of course, this won't tell them "what it means", but it still gives someone information about it...
crypt = Crypt.new("my-not-so-amazing-password")
net = WSApplication.current_network
net.transaction_begin
net.each do |obj|
if obj.responds_to? :comment
if obj.comment != ""
obj.comment = crypt.encrypt(obj.comment)
obj.write
end
end
end
net.transaction_commit
And to decrypt again:
crypt = Crypt.new("my-not-so-amazing-password")
net = WSApplication.current_network
net.transaction_begin
net.each do |obj|
if obj.responds_to? :comment
if obj.comment != ""
obj.comment = crypt.decrypt(obj.comment)
obj.write
end
end
end
net.transaction_commit
Special thanks to:
Due to the nature of this operation data can be completely destroyed if the algorithms are used by people who do not know what they are doing. Before testing any of the algorithms make sure to make a back up!
ALL CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.