From 54d7be8a49cbbd47217df84c131221b19e66f0e3 Mon Sep 17 00:00:00 2001 From: Adam Frisby Date: Tue, 14 Oct 2008 08:54:46 +0000 Subject: * Adding CrytoGridAssetClient support - allows encrypting assets that are stored on a potentially hostile grid. This is not DRM, not should be relied on until after it's been security audited. I'll write a blog post on this explaining how/why/when you should use this, and what it does. --- .../Communications/Cache/CryptoGridAssetClient.cs | 527 +++++++++++++++++++++ OpenSim/Region/Application/OpenSimBase.cs | 10 + 2 files changed, 537 insertions(+) create mode 100644 OpenSim/Framework/Communications/Cache/CryptoGridAssetClient.cs (limited to 'OpenSim') diff --git a/OpenSim/Framework/Communications/Cache/CryptoGridAssetClient.cs b/OpenSim/Framework/Communications/Cache/CryptoGridAssetClient.cs new file mode 100644 index 0000000..a8e6efb --- /dev/null +++ b/OpenSim/Framework/Communications/Cache/CryptoGridAssetClient.cs @@ -0,0 +1,527 @@ +/* + * 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). + byte[] keyBytes = password.GetBytes(keySize / 8); + + // 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). + byte[] keyBytes = password.GetBytes(keySize / 8); + + // 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.Description = String.Format("ENCASS#:~:#{0}#:~:#{1}#:~:#{2}#:~:#{3}", + "OPENSIM_AES_AF1", + file.AlsoKnownAs, + salt, + x.Description); + } + + private bool DecryptAssetBase(AssetBase x) + { + // Check it's encrypted first. + if (!x.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.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.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.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 + } +} diff --git a/OpenSim/Region/Application/OpenSimBase.cs b/OpenSim/Region/Application/OpenSimBase.cs index 7598e16..eff2422 100644 --- a/OpenSim/Region/Application/OpenSimBase.cs +++ b/OpenSim/Region/Application/OpenSimBase.cs @@ -418,6 +418,16 @@ namespace OpenSim { assetServer = new GridAssetClient(m_networkServersInfo.AssetURL); } + else if (m_assetStorage == "cryptogrid") // Decrypt-Only + { + assetServer = new CryptoGridAssetClient(m_networkServersInfo.AssetURL, + Environment.CurrentDirectory, true); + } + else if (m_assetStorage == "cryptogrid_eou") // Encrypts All Assets + { + assetServer = new CryptoGridAssetClient(m_networkServersInfo.AssetURL, + Environment.CurrentDirectory, false); + } else if (m_assetStorage == "file") { assetServer = new FileAssetClient(m_networkServersInfo.AssetURL); -- cgit v1.1