diff --git a/ShareX.UploadersLib/FileUploaders/Up1.cs b/ShareX.UploadersLib/FileUploaders/Up1.cs index 69d6084bc..7862bb96c 100644 --- a/ShareX.UploadersLib/FileUploaders/Up1.cs +++ b/ShareX.UploadersLib/FileUploaders/Up1.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -10,9 +11,19 @@ using Org.BouncyCastle.Crypto.Parameters; using ShareX.HelpersLib; - namespace ShareX.UploadersLib.FileUploaders { + /* + * Up1 is an encrypted image uploader. The idea is that any URL (for example, https://up1.ca/#hsd2mdSuIkzTUR6saZpn1Q) contains + * what is called a "seed". In this case, the seed is "hsd2mdSuIkzTUR6saZpn1Q". With this, we use sha512(seed) (output 64 bytes) + * in order to derive an AES key (32 bytes), a CCM IV (16 bytes), and a server-side identifier (16 bytes). These are used to + * encrypt and store the data. + * + * Within the encrypted blob, There is a double-null-terminated UTF-16 JSON object that contains metadata like the filename and mimetype. + * This is prepended to the image data. + */ + + [Localizable(false)] public sealed class Up1 : FileUploader { private const int MacSize = 64; @@ -26,89 +37,270 @@ public Up1(string systemUrl, string apiKey) ApiKey = apiKey; } - + /* Since we're dealing with URLs, the regular base64 encoding using +, / and = will mess things up + * Therefore we'll use the URL base64 standard (as defined in the RFC) + */ public static string UrlBase64Encode(byte[] input) { return Convert.ToBase64String(input).Replace("=", "").Replace("+", "-").Replace("/", "_"); } - public static Stream Encrypt(Stream stream, out string seed_encoded, out string ident_encoded, string fileName) - { - RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider(); - byte[] seed = new byte[16]; - rngCsp.GetBytes(seed); - seed_encoded = UrlBase64Encode(seed); - - SHA512CryptoServiceProvider sha512csp = new SHA512CryptoServiceProvider(); - byte[] seed_result = sha512csp.ComputeHash(seed); - byte[] key = new byte[32]; - Buffer.BlockCopy(seed_result, 0, key, 0, 32); - - byte[] iv = new byte[16]; - Buffer.BlockCopy(seed_result, 32, iv, 0, 16); - - byte[] ident = new byte[16]; - Buffer.BlockCopy(seed_result, 48, ident, 0, 16); - ident_encoded = UrlBase64Encode(ident); - var fi = new FileInfo(fileName); - - Dictionary args = new Dictionary(); - - // text files aren't detected well by the "ClouDeveloper" mime type library, use ShareX's builtin list first. - if (Helpers.IsTextFile(fileName)) - { - args["mime"] = "text/plain"; - } - else - { - var mimeOpts = ClouDeveloper.Mime.MediaTypeNames.GetMediaTypeNames(fi.Extension).ToList(); - args["mime"] = mimeOpts.Count > 0 ? mimeOpts[0] : "image/png"; - } - args["name"] = fileName; - - byte[] d = Encoding.BigEndianUnicode.GetBytes(JsonConvert.SerializeObject(args)); - - byte[] rawdata = d.Concat(new byte[] { 0, 0 }).Concat(stream.GetBytes()).ToArray(); - - int l = FindIVLen(rawdata.Length); - byte[] civ = new byte[l]; - Array.Copy(iv, civ, l); - KeyParameter key_param = new KeyParameter(key); - var ccmparams = new CcmParameters(key_param, MacSize, civ, new byte[0]); - var ccmMode = new CcmBlockCipher(new AesFastEngine()); - ccmMode.Init(true, ccmparams); - var encBytes = new byte[ccmMode.GetOutputSize(rawdata.Length)]; - var res = ccmMode.ProcessBytes(rawdata, 0, rawdata.Length, encBytes, 0); - ccmMode.DoFinal(encBytes, res); - - return new MemoryStream(encBytes); - } - - private static int FindIVLen(int bufferLength) + /* SJCL (the Javascript library powering the crypto in the browser) depends on the length of the input + * in order to calculate CCM's IV length. This is the algorithm it uses. + */ + private static long FindIVLen(long bufferLength) { if (bufferLength < 0xFFFF) return 15 - 2; if (bufferLength < 0xFFFFFF) return 15 - 3; return 15 - 4; } - - public override UploadResult Upload(Stream stream, string fileName) + /* Given an input seed, derive the required output. + */ + public static void DeriveParams(byte[] seed, out byte[] key, out byte[] iv, out string ident) { - string seed, ident; - Stream encryptedStream = Encrypt(stream, out seed, out ident, fileName); - Dictionary args = new Dictionary(); - args["ident"] = ident; - args["api_key"] = ApiKey; - UploadResult result = UploadData(encryptedStream, SystemUrl + "/up", "blob", "file", args); + // Hash the output using sha512 + var sha512csp = new SHA512CryptoServiceProvider(); + var seed_output = sha512csp.ComputeHash(seed); - if (result.IsSuccess) - { - Dictionary values = JsonConvert.DeserializeObject>(result.Response); - result.URL = SystemUrl + "/#" + seed; - result.DeletionURL = SystemUrl + "/del?ident=" + ident + "&delkey=" + values["delkey"]; - } + // Take key from first 32 bytes + key = new byte[32]; + Buffer.BlockCopy(seed_output, 0, key, 0, 32); + + // Take IV from next 16 bytes + iv = new byte[16]; + Buffer.BlockCopy(seed_output, 32, iv, 0, 16); + + // Take server identifier (the "ident") from last 16 bytes and base64url encode it + var ident_raw = new byte[16]; + Buffer.BlockCopy(seed_output, 48, ident_raw, 0, 16); + ident = UrlBase64Encode(ident_raw); + } + + public static Stream Encrypt(Stream source, string fileName, out string seed_encoded, out string ident) + { + // Randomly generate a new seed for upload + var rngCsp = new RNGCryptoServiceProvider(); + var seed = new byte[16]; + rngCsp.GetBytes(seed); + seed_encoded = UrlBase64Encode(seed); + + // Derive the parameters (key, IV, ident) from the seed + byte[] key, iv; + DeriveParams(seed, out key, out iv, out ident); + + // Create a new String->String map for JSON blob, and define filename and metadata + var metadataMap = new Dictionary(); + metadataMap["mime"] = Helpers.IsTextFile(fileName) ? "text/plain" : Helpers.GetMimeType(fileName); + metadataMap["name"] = fileName; + + // Encode the metadata with UTF-16 and a double-null-byte terminator + var metadata = Encoding.BigEndianUnicode.GetBytes(JsonConvert.SerializeObject(metadataMap)).Concat(new byte[] { 0, 0 }).ToArray(); + + // Calculate the length of the CCM IV and copy it over + var ccmIVLen = FindIVLen(metadata.Length + source.Length); + var ccmIV = new byte[ccmIVLen]; + Array.Copy(iv, ccmIV, ccmIVLen); + + // Set up the encryption parameters + var keyParam = new KeyParameter(key); + var ccmParams = new CcmParameters(keyParam, MacSize, ccmIV, new byte[0]); + var ccmMode = new CcmBlockCipher(new AesFastEngine()); + ccmMode.Init(true, ccmParams); + + return new Up1Stream(source, metadata, ccmMode, metadata.Length + (int)source.Length); + } + + public override UploadResult Upload(Stream input, string fileName) + { + // Initialize the encrypted stream + string seed, ident; + var encryptedStream = Encrypt(input, fileName, out seed, out ident); + + // Set up the form upload + var uploadArgs = new Dictionary(); + uploadArgs["ident"] = ident; + uploadArgs["api_key"] = ApiKey; + + // Upload and stream encrypt + var result = UploadData(encryptedStream, SystemUrl + "/up", "blob", "file", uploadArgs); + + if (!result.IsSuccess) return result; + + // Return the output URLs + var values = JsonConvert.DeserializeObject>(result.Response); + result.URL = SystemUrl + "/#" + seed; + result.DeletionURL = SystemUrl + "/del?ident=" + ident + "&delkey=" + values["delkey"]; return result; } } + + /* This custom stream is used to encrypt the data on-the-fly. As the CCM functions in BouncyCastle are designed to + * have a known input and unknown output, there are functions to accommodate this (GetOutputSize). Unfortunately as + * a Stream we are left with the other way around (unknown input and known output). In this case, we need to use an + * overrun buffer, as we can't perfectly estimate the size of intermediate buffers. + */ + sealed class Up1Stream : Stream + { + private Stream _source; + private byte[] _overrun; + private CcmBlockCipher _ccmMode; + private bool _isCCMFinal; + private long _length; + + public Up1Stream(Stream source, byte[] metadata, CcmBlockCipher ccmMode, long length) + { + _source = source; + _ccmMode = ccmMode; + _overrun = EncryptMetadata(metadata); // Nice little hack to ensure the metadata is written first + _isCCMFinal = false; + _length = length; + } + + public byte[] EncryptMetadata(byte[] metadata) + { + var inArray = new byte[metadata.Length]; + Array.Copy(metadata, inArray, metadata.Length); + var metaLen = _ccmMode.GetOutputSize(metadata.Length); + var outMeta = new byte[metaLen]; + var outRealLen = _ccmMode.ProcessBytes(inArray, 0, inArray.Length, outMeta, 0); + + Array.Resize(ref outMeta, outRealLen); + return outMeta; + } + + public override int Read(byte[] buffer, int offset, int count) + { + // Check if the overrun is enough to satisfy the buffer + if (_overrun.Length >= count) + { + Array.Copy(_overrun, 0, buffer, offset, count); + _overrun = _overrun.Skip(count).ToArray(); + return count; + } + + // Check if we're on the last legs (CCM has been finalized) + if (_isCCMFinal) + { + if (_overrun.Length != 0) + Array.Copy(_overrun, 0, buffer, offset, _overrun.Length); + return _overrun.Length; + } + + // If overrun contains anything, throw it in the buffer immediately + var remainCount = count; + var remainOffset = offset; + if (_overrun.Length != 0) + { + remainOffset += _overrun.Length; + remainCount -= _overrun.Length; + Array.Copy(_overrun, buffer, _overrun.Length); + // At this point, ignore anything in _overrun + } + + // Read chunk of data from input + var inBytes = new byte[remainCount]; + var readBytes = _source.Read(inBytes, 0, remainCount); + + // Calculate how much length we need depending on if we're at the end or not + var encLength = _ccmMode.GetOutputSize(readBytes); + + // Close the file and finalize if we're at the end of the input + if (readBytes == 0) + { + _source.Close(); + _isCCMFinal = true; + } + + // Create overrun buffer if we have too much output + if (encLength > remainCount) + { + _overrun = new byte[encLength - remainCount]; + var encBytes = new byte[encLength]; + if (!_isCCMFinal) + _ccmMode.ProcessBytes(inBytes, 0, readBytes, encBytes, 0); + else + _ccmMode.DoFinal(encBytes, 0); + + Array.Copy(encBytes, 0, buffer, remainOffset, remainCount); + Array.Copy(encBytes, encLength - remainCount, _overrun, 0, encLength - remainCount); + } + // Otherwise, encrypt directly to the given buffer + else + { + if (_overrun.Length != 0) + _overrun = new byte[0]; // Clear the overrun buffer if exists + if (!_isCCMFinal) + _ccmMode.ProcessBytes(inBytes, 0, readBytes, buffer, remainOffset); + else + _ccmMode.DoFinal(buffer, remainOffset); + } + + return remainCount; + } + + public override bool CanRead + { + get + { + return true; + } + } + public override bool CanSeek + { + get + { + return false; + } + } + public override bool CanWrite + { + get + { + return false; + } + } + + // We can't implement anything else in this stream + + public override long Length + { + get + { + return _length; + } + } + public override long Position + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + } }