/*
* Copyright (c) Contributors, http://www.openmetaverse.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSim Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
* This file includes content derived from Obviex.
* Copyright (C) 2002 Obviex(TM). All rights reserved.
* http://www.obviex.com/samples/Encryption.aspx
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Xml.Serialization;
using log4net;
using OpenSim.Framework.Servers;
using System.Security.Cryptography;
namespace OpenSim.Framework.Communications.Cache
{
public class CryptoGridAssetClient : AssetServerBase
{
#region Keyfile Classes
[Serializable]
private class RjinKeyfile
{
public string Secret;
public string AlsoKnownAs;
public int Keysize;
public string IVBytes;
public string Description = "OpenSim Key";
private static string SHA1Hash(byte[] bytes)
{
SHA1 sha1 = SHA1CryptoServiceProvider.Create();
byte[] dataMd5 = sha1.ComputeHash(bytes);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < dataMd5.Length; i++)
sb.AppendFormat("{0:x2}", dataMd5[i]);
return sb.ToString();
}
public void GenerateRandom()
{
RNGCryptoServiceProvider Gen = new RNGCryptoServiceProvider();
byte[] genSec = new byte[32];
byte[] genAKA = new byte[32];
byte[] genIV = new byte[32];
Gen.GetBytes(genSec);
Gen.GetBytes(genAKA);
Gen.GetBytes(genIV);
Secret = SHA1Hash(genSec);
AlsoKnownAs = SHA1Hash(genAKA);
IVBytes = SHA1Hash(genIV).Substring(0, 16);
Keysize = 256;
}
}
#endregion
#region Rjindael
///
/// This class uses a symmetric key algorithm (Rijndael/AES) to encrypt and
/// decrypt data. As long as encryption and decryption routines use the same
/// parameters to generate the keys, the keys are guaranteed to be the same.
/// The class uses static functions with duplicate code to make it easier to
/// demonstrate encryption and decryption logic. In a real-life application,
/// this may not be the most efficient way of handling encryption, so - as
/// soon as you feel comfortable with it - you may want to redesign this class.
///
private class UtilRijndael
{
///
/// Encrypts specified plaintext using Rijndael symmetric key algorithm
/// and returns a base64-encoded result.
///
///
/// Plaintext value to be encrypted.
///
///
/// Passphrase from which a pseudo-random password will be derived. The
/// derived password will be used to generate the encryption key.
/// Passphrase can be any string. In this example we assume that this
/// passphrase is an ASCII string.
///
///
/// Salt value used along with passphrase to generate password. Salt can
/// be any string. In this example we assume that salt is an ASCII string.
///
///
/// Hash algorithm used to generate password. Allowed values are: "MD5" and
/// "SHA1". SHA1 hashes are a bit slower, but more secure than MD5 hashes.
///
///
/// Number of iterations used to generate password. One or two iterations
/// should be enough.
///
///
/// Initialization vector (or IV). This value is required to encrypt the
/// first block of plaintext data. For RijndaelManaged class IV must be
/// exactly 16 ASCII characters long.
///
///
/// Size of encryption key in bits. Allowed values are: 128, 192, and 256.
/// Longer keys are more secure than shorter keys.
///
///
/// Encrypted value formatted as a base64-encoded string.
///
public static byte[] Encrypt(byte[] plainText,
string passPhrase,
string saltValue,
string hashAlgorithm,
int passwordIterations,
string initVector,
int keySize)
{
// Convert strings into byte arrays.
// Let us assume that strings only contain ASCII codes.
// If strings include Unicode characters, use Unicode, UTF7, or UTF8
// encoding.
byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);
byte[] saltValueBytes = Encoding.ASCII.GetBytes(saltValue);
// Convert our plaintext into a byte array.
// Let us assume that plaintext contains UTF8-encoded characters.
byte[] plainTextBytes = plainText;
// First, we must create a password, from which the key will be derived.
// This password will be generated from the specified passphrase and
// salt value. The password will be created using the specified hash
// algorithm. Password creation can be done in several iterations.
PasswordDeriveBytes password = new PasswordDeriveBytes(
passPhrase,
saltValueBytes,
hashAlgorithm,
passwordIterations);
// Use the password to generate pseudo-random bytes for the encryption
// key. Specify the size of the key in bytes (instead
// of bits).
#pragma warning disable 0618
byte[] keyBytes = password.GetBytes(keySize / 8);
#pragma warning restore 0618
// Create uninitialized Rijndael encryption object.
RijndaelManaged symmetricKey = new RijndaelManaged();
// It is reasonable to set encryption mode to Cipher Block Chaining
// (CBC). Use default options for other symmetric key parameters.
symmetricKey.Mode = CipherMode.CBC;
// Generate encryptor from the existing key bytes and initialization
// vector. Key size will be defined based on the number of the key
// bytes.
ICryptoTransform encryptor = symmetricKey.CreateEncryptor(
keyBytes,
initVectorBytes);
// Define memory stream which will be used to hold encrypted data.
MemoryStream memoryStream = new MemoryStream();
// Define cryptographic stream (always use Write mode for encryption).
CryptoStream cryptoStream = new CryptoStream(memoryStream,
encryptor,
CryptoStreamMode.Write);
// Start encrypting.
cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
// Finish encrypting.
cryptoStream.FlushFinalBlock();
// Convert our encrypted data from a memory stream into a byte array.
byte[] cipherTextBytes = memoryStream.ToArray();
// Close both streams.
memoryStream.Close();
cryptoStream.Close();
// Return encrypted string.
return cipherTextBytes;
}
///
/// Decrypts specified ciphertext using Rijndael symmetric key algorithm.
///
///
/// Base64-formatted ciphertext value.
///
///
/// Passphrase from which a pseudo-random password will be derived. The
/// derived password will be used to generate the encryption key.
/// Passphrase can be any string. In this example we assume that this
/// passphrase is an ASCII string.
///
///
/// Salt value used along with passphrase to generate password. Salt can
/// be any string. In this example we assume that salt is an ASCII string.
///
///
/// Hash algorithm used to generate password. Allowed values are: "MD5" and
/// "SHA1". SHA1 hashes are a bit slower, but more secure than MD5 hashes.
///
///
/// Number of iterations used to generate password. One or two iterations
/// should be enough.
///
///
/// Initialization vector (or IV). This value is required to encrypt the
/// first block of plaintext data. For RijndaelManaged class IV must be
/// exactly 16 ASCII characters long.
///
///
/// Size of encryption key in bits. Allowed values are: 128, 192, and 256.
/// Longer keys are more secure than shorter keys.
///
///
/// Decrypted string value.
///
///
/// Most of the logic in this function is similar to the Encrypt
/// logic. In order for decryption to work, all parameters of this function
/// - except cipherText value - must match the corresponding parameters of
/// the Encrypt function which was called to generate the
/// ciphertext.
///
public static byte[] Decrypt(byte[] cipherText,
string passPhrase,
string saltValue,
string hashAlgorithm,
int passwordIterations,
string initVector,
int keySize)
{
// Convert strings defining encryption key characteristics into byte
// arrays. Let us assume that strings only contain ASCII codes.
// If strings include Unicode characters, use Unicode, UTF7, or UTF8
// encoding.
byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);
byte[] saltValueBytes = Encoding.ASCII.GetBytes(saltValue);
// Convert our ciphertext into a byte array.
byte[] cipherTextBytes = cipherText;
// First, we must create a password, from which the key will be
// derived. This password will be generated from the specified
// passphrase and salt value. The password will be created using
// the specified hash algorithm. Password creation can be done in
// several iterations.
PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase,
saltValueBytes,
hashAlgorithm,
passwordIterations);
// Use the password to generate pseudo-random bytes for the encryption
// key. Specify the size of the key in bytes (instead
// of bits).
#pragma warning disable 0618
byte[] keyBytes = password.GetBytes(keySize / 8);
#pragma warning restore 0618
// Create uninitialized Rijndael encryption object.
RijndaelManaged symmetricKey = new RijndaelManaged();
// It is reasonable to set encryption mode to Cipher Block Chaining
// (CBC). Use default options for other symmetric key parameters.
symmetricKey.Mode = CipherMode.CBC;
// Generate decryptor from the existing key bytes and initialization
// vector. Key size will be defined based on the number of the key
// bytes.
ICryptoTransform decryptor = symmetricKey.CreateDecryptor(
keyBytes,
initVectorBytes);
// Define memory stream which will be used to hold encrypted data.
MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
// Define cryptographic stream (always use Read mode for encryption).
CryptoStream cryptoStream = new CryptoStream(memoryStream,
decryptor,
CryptoStreamMode.Read);
// Since at this point we don't know what the size of decrypted data
// will be, allocate the buffer long enough to hold ciphertext;
// plaintext is never longer than ciphertext.
byte[] plainTextBytes = new byte[cipherTextBytes.Length];
// Start decrypting.
int decryptedByteCount = cryptoStream.Read(plainTextBytes,
0,
plainTextBytes.Length);
// Close both streams.
memoryStream.Close();
cryptoStream.Close();
byte[] plainText = new byte[decryptedByteCount];
int i;
for (i = 0; i < decryptedByteCount; i++)
plainText[i] = plainTextBytes[i];
// Return decrypted string.
return plainText;
}
}
#endregion
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private readonly string _assetServerUrl;
private readonly bool m_encryptOnUpload;
private readonly RjinKeyfile m_encryptKey;
private readonly Dictionary m_keyfiles = new Dictionary();
public CryptoGridAssetClient(string serverUrl, string keydir, bool decOnly)
{
_assetServerUrl = serverUrl;
string[] keys = Directory.GetFiles(keydir, "*.deckey");
foreach (string key in keys)
{
XmlSerializer xs = new XmlSerializer(typeof (RjinKeyfile));
FileStream file = new FileStream(key, FileMode.Open, FileAccess.Read);
RjinKeyfile rjkey = (RjinKeyfile) xs.Deserialize(file);
file.Close();
m_keyfiles.Add(rjkey.AlsoKnownAs, rjkey);
}
keys = Directory.GetFiles(keydir, "*.enckey");
if (keys.Length == 1)
{
string Ekey = keys[0];
XmlSerializer Exs = new XmlSerializer(typeof (RjinKeyfile));
FileStream Efile = new FileStream(Ekey, FileMode.Open, FileAccess.Read);
RjinKeyfile Erjkey = (RjinKeyfile) Exs.Deserialize(Efile);
Efile.Close();
m_keyfiles.Add(Erjkey.AlsoKnownAs, Erjkey);
m_encryptKey = Erjkey;
} else
{
if (keys.Length > 1)
throw new Exception(
"You have more than one asset *encryption* key. (You should never have more than one)," +
"If you downloaded this key from someone, rename it to .deckey to convert it to" +
"a decryption-only key.");
m_log.Warn("No encryption key found, generating a new one for you...");
RjinKeyfile encKey = new RjinKeyfile();
encKey.GenerateRandom();
m_encryptKey = encKey;
FileStream encExportFile = new FileStream("mysecretkey_rename_me.enckey",FileMode.CreateNew);
XmlSerializer xs = new XmlSerializer(typeof(RjinKeyfile));
xs.Serialize(encExportFile, encKey);
encExportFile.Flush();
encExportFile.Close();
m_log.Info(
"Encryption file generated, please rename 'mysecretkey_rename_me.enckey' to something more appropriate (however preserve the file extension).");
}
// If Decrypt-Only, dont encrypt on upload
m_encryptOnUpload = !decOnly;
}
private static void EncryptAssetBase(AssetBase x, RjinKeyfile file)
{
// Make a salt
RNGCryptoServiceProvider RandomGen = new RNGCryptoServiceProvider();
byte[] rand = new byte[32];
RandomGen.GetBytes(rand);
string salt = Convert.ToBase64String(rand);
x.Data = UtilRijndael.Encrypt(x.Data, file.Secret, salt, "SHA1", 2, file.IVBytes, file.Keysize);
x.Metadata.Description = String.Format("ENCASS#:~:#{0}#:~:#{1}#:~:#{2}#:~:#{3}",
"OPENSIM_AES_AF1",
file.AlsoKnownAs,
salt,
x.Metadata.Description);
}
private bool DecryptAssetBase(AssetBase x)
{
// Check it's encrypted first.
if (!x.Metadata.Description.Contains("ENCASS"))
return true;
// ENCASS:ALG:AKA:SALT:Description
// 0 1 2 3 4
string[] splitchars = new string[1];
splitchars[0] = "#:~:#";
string[] meta = x.Metadata.Description.Split(splitchars, StringSplitOptions.None);
if (meta.Length < 5)
{
m_log.Warn("[ENCASSETS] Recieved Encrypted Asset, but header is corrupt");
return false;
}
// Check if we have a matching key
if (m_keyfiles.ContainsKey(meta[2]))
{
RjinKeyfile deckey = m_keyfiles[meta[2]];
x.Metadata.Description = meta[4];
switch (meta[1])
{
case "OPENSIM_AES_AF1":
x.Data = UtilRijndael.Decrypt(x.Data,
deckey.Secret,
meta[3],
"SHA1",
2,
deckey.IVBytes,
deckey.Keysize);
// Decrypted Successfully
return true;
default:
m_log.Warn(
"[ENCASSETS] Recieved Encrypted Asset, but we dont know how to decrypt '" + meta[1] + "'.");
// We dont understand this encryption scheme
return false;
}
}
m_log.Warn("[ENCASSETS] Recieved Encrypted Asset, but we do not have the decryption key.");
return false;
}
#region IAssetServer Members
protected override AssetBase GetAsset(AssetRequest req)
{
#if DEBUG
//m_log.DebugFormat("[GRID ASSET CLIENT]: Querying for {0}", req.AssetID.ToString());
#endif
RestClient rc = new RestClient(_assetServerUrl);
rc.AddResourcePath("assets");
rc.AddResourcePath(req.AssetID.ToString());
if (req.IsTexture)
rc.AddQueryParameter("texture");
rc.RequestMethod = "GET";
Stream s = rc.Request();
if (s == null)
return null;
if (s.Length > 0)
{
XmlSerializer xs = new XmlSerializer(typeof(AssetBase));
AssetBase encAsset = (AssetBase)xs.Deserialize(s);
// Try decrypt it
if (DecryptAssetBase(encAsset))
return encAsset;
}
return null;
}
public override void UpdateAsset(AssetBase asset)
{
throw new Exception("The method or operation is not implemented.");
}
public override void StoreAsset(AssetBase asset)
{
if (m_encryptOnUpload)
EncryptAssetBase(asset, m_encryptKey);
try
{
string assetUrl = _assetServerUrl + "/assets/";
m_log.InfoFormat("[CRYPTO GRID ASSET CLIENT]: Sending store request for asset {0}", asset.Metadata.FullID);
RestObjectPoster.BeginPostObject(assetUrl, asset);
}
catch (Exception e)
{
m_log.ErrorFormat("[CRYPTO GRID ASSET CLIENT]: {0}", e);
}
}
public override void Close()
{
throw new Exception("The method or operation is not implemented.");
}
#endregion
}
}