2015-06-29 04:16:22 +12:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2015-06-29 08:59:30 +12:00
|
|
|
|
using System.ComponentModel;
|
2015-06-29 04:16:22 +12:00
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
using Org.BouncyCastle.Crypto.Engines;
|
|
|
|
|
using Org.BouncyCastle.Crypto.Modes;
|
|
|
|
|
using Org.BouncyCastle.Crypto.Parameters;
|
|
|
|
|
using ShareX.HelpersLib;
|
|
|
|
|
|
|
|
|
|
namespace ShareX.UploadersLib.FileUploaders
|
|
|
|
|
{
|
2015-06-29 08:59:30 +12:00
|
|
|
|
/*
|
|
|
|
|
* 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)]
|
2015-06-29 04:16:22 +12:00
|
|
|
|
public sealed class Up1 : FileUploader
|
|
|
|
|
{
|
|
|
|
|
private const int MacSize = 64;
|
|
|
|
|
|
|
|
|
|
public string ApiKey { get; set; }
|
|
|
|
|
public string SystemUrl { get; set; }
|
|
|
|
|
|
|
|
|
|
public Up1(string systemUrl, string apiKey)
|
|
|
|
|
{
|
|
|
|
|
SystemUrl = systemUrl;
|
|
|
|
|
ApiKey = apiKey;
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-29 08:59:30 +12:00
|
|
|
|
/* 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)
|
|
|
|
|
*/
|
2015-06-29 04:16:22 +12:00
|
|
|
|
public static string UrlBase64Encode(byte[] input)
|
|
|
|
|
{
|
|
|
|
|
return Convert.ToBase64String(input).Replace("=", "").Replace("+", "-").Replace("/", "_");
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-29 08:59:30 +12:00
|
|
|
|
/* 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Given an input seed, derive the required output.
|
|
|
|
|
*/
|
|
|
|
|
public static void DeriveParams(byte[] seed, out byte[] key, out byte[] iv, out string ident)
|
2015-06-29 04:16:22 +12:00
|
|
|
|
{
|
2015-06-29 08:59:30 +12:00
|
|
|
|
// Hash the output using sha512
|
|
|
|
|
var sha512csp = new SHA512CryptoServiceProvider();
|
|
|
|
|
var seed_output = sha512csp.ComputeHash(seed);
|
|
|
|
|
|
|
|
|
|
// 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];
|
2015-06-29 04:16:22 +12:00
|
|
|
|
rngCsp.GetBytes(seed);
|
|
|
|
|
seed_encoded = UrlBase64Encode(seed);
|
|
|
|
|
|
2015-06-29 08:59:30 +12:00
|
|
|
|
// 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<string, string>();
|
|
|
|
|
metadataMap["mime"] = Helpers.IsTextFile(fileName) ? "text/plain" : Helpers.GetMimeType(fileName);
|
|
|
|
|
metadataMap["name"] = fileName;
|
|
|
|
|
|
2015-06-29 09:10:14 +12:00
|
|
|
|
// Encode the metadata with UTF-16 and a double-null-byte terminator, and append data
|
|
|
|
|
// Unfortunately, the CCM cipher mode can't stream the encryption, and so we have to GetBytes() on the source.
|
|
|
|
|
// We do limit the source to 50MB however
|
|
|
|
|
var data = Encoding.BigEndianUnicode.GetBytes(JsonConvert.SerializeObject(metadataMap)).Concat(new byte[] { 0, 0 }).Concat(source.GetBytes()).ToArray();
|
2015-06-29 04:16:22 +12:00
|
|
|
|
|
2015-06-29 08:59:30 +12:00
|
|
|
|
// Calculate the length of the CCM IV and copy it over
|
2015-06-29 09:10:14 +12:00
|
|
|
|
var ccmIVLen = FindIVLen(data.Length);
|
2015-06-29 08:59:30 +12:00
|
|
|
|
var ccmIV = new byte[ccmIVLen];
|
|
|
|
|
Array.Copy(iv, ccmIV, ccmIVLen);
|
2015-06-29 04:16:22 +12:00
|
|
|
|
|
2015-06-29 08:59:30 +12:00
|
|
|
|
// 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);
|
|
|
|
|
|
2015-06-29 09:10:14 +12:00
|
|
|
|
// Perform the encryption
|
|
|
|
|
var encBytes = new byte[ccmMode.GetOutputSize(data.Length)];
|
|
|
|
|
var res = ccmMode.ProcessBytes(data, 0, data.Length, encBytes, 0);
|
|
|
|
|
ccmMode.DoFinal(encBytes, res);
|
|
|
|
|
|
|
|
|
|
return new MemoryStream(encBytes);
|
2015-06-29 08:59:30 +12:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override UploadResult Upload(Stream input, string fileName)
|
|
|
|
|
{
|
2015-06-29 09:10:14 +12:00
|
|
|
|
// Make sure the file (or memory stream) is less than 50MB
|
|
|
|
|
if (input.Length > 50000000)
|
|
|
|
|
throw new ArgumentException("Input files for Up1 cannot be more than 50MB in size");
|
|
|
|
|
|
2015-06-29 08:59:30 +12:00
|
|
|
|
// 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<string, string>();
|
|
|
|
|
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<Dictionary<string, string>>(result.Response);
|
|
|
|
|
result.URL = SystemUrl + "/#" + seed;
|
|
|
|
|
result.DeletionURL = SystemUrl + "/del?ident=" + ident + "&delkey=" + values["delkey"];
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
2015-06-29 23:38:47 +12:00
|
|
|
|
}
|
|
|
|
|
}
|