Skip to content

Instantly share code, notes, and snippets.

@exelix11
Last active April 23, 2019 11:40
Show Gist options
  • Save exelix11/72b2c45cceedd451d414c3e3ad26d2a5 to your computer and use it in GitHub Desktop.
Save exelix11/72b2c45cceedd451d414c3e3ad26d2a5 to your computer and use it in GitHub Desktop.
Yi cloud api RE
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace YiCli
{
public static class crypt_file
{
//decrypt files retrieved from their apis
public static byte[] decryptFile(byte[] data, string key)
{
uint originalSize = BitConverter.ToUInt32(data, 0); //Todo make sure this is loaded as little endian
byte[] Key = Encoding.ASCII.GetBytes(key);
if (Key.Length != 16) throw new Exception();
var aes = new RijndaelManaged();
aes.BlockSize = 128;
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
aes.IV = new byte[16];
aes.Key = Key;
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
byte[] dec = decryptor.TransformFinalBlock(data, 4, data.Length - 4);
if (originalSize == dec.Length)
return dec;
byte[] res = new byte[originalSize]; //the encrypted file is 0-padded
Buffer.BlockCopy(dec, 0, res, 0, (int)originalSize);
return res;
}
//Untested but is it really needed
public static byte[] encryptFile(byte[] data, string key)
{
uint originalSize = (uint)data.Length;
byte[] Key = Encoding.ASCII.GetBytes(key);
if (Key.Length != 16) throw new Exception();
var aes = new RijndaelManaged();
aes.BlockSize = 128;
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
aes.IV = new byte[16];
aes.Key = Key;
byte[] actualData = null;
if (originalSize % 15 == 0)
actualData = data;
else
{
actualData = new byte[originalSize + originalSize % 15];
Buffer.BlockCopy(data, 0, actualData, 0, (int)originalSize);
}
ICryptoTransform enc = aes.CreateEncryptor(aes.Key, aes.IV);
actualData = enc.TransformFinalBlock(actualData, 0, actualData.Length);
byte[] res = new byte[actualData.Length + 4];
Buffer.BlockCopy(BitConverter.GetBytes(originalSize), 0, res, 0, 4);
Buffer.BlockCopy(actualData, 0, res, 4, actualData.Length);
return res;
}
}
}
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.IO;
using System.Linq;
using System.Collections;
namespace YiCli
{
static class WebReqHelper
{
public static string TOKEN;
public static string TOKEN_SECRET;
const string API_URL = "https://api.eu.xiaoyi.com";
public static HttpWebRequest MakeRequest(string path, Dictionary<string, string> Args)
{
string hmac = CalculateHmac(Args, TOKEN + "&" + TOKEN_SECRET);
hmac = hmac.Replace("+", "%2B"); //This seems to be the only char that is escaped, / and = don't change
Args.Add("hmac", hmac);
string url = $"{API_URL}{path}";
string args = "";
foreach (var k in Args.Keys.Reverse())
args += $"&{k}={Args[k]}";
url += "?" + args.Substring(1);
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
AddHeaders(ref req);
return req;
}
public static void AddHeaders(ref HttpWebRequest req)
{
req.Headers["user-agent"] = "YI Home/4.0.5_20190318 (device; Android 9; en-GB)";
req.Headers["x-xiaoyi-appversion"] = "android;105;4.0.5_20190318";
}
public static string CalculateHmac(Dictionary<string, string> args, string key)
{
string toHash = "";
foreach (var k in args.Keys)
{
toHash += $"&{k}={args[k]}";
}
if (toHash.Length > 0)
toHash = toHash.Substring(1);
return DoHmac(toHash, key);
}
static string DoHmac(string str, string key)
{
HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(key));
return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(str)));
}
}
class Constants
{
public const string KEY_KEEP_COOKIES = "extra_keep_cookies ";
public const string KEY_MY_BUNDLE = "extra_my_bundle";
public const string KEY_MY_INTENT = "extra_my_intent";
public const string KEY_REDIRECT_URI = "redirect_uri";
public const string KEY_RESPONSE = "extra_response";
public const string KEY_RESULT_CODE = "extra_result_code";
public const string KEY_SERVICETOKEN = "serviceToken";
public const string KEY_USERID = "userid";
}
static class KeyInitialization
{
//grab these from ProfileV2.xml in the shared settings of Yi home
const string TOKEN = "your encrypted token";
const string TOKEN_SECRET = "your encrypted secret token";
// Seems to be generated from:
// "35" + (Build.BOARD.length() % 10) + (Build.BRAND.length() % 10) + (Build.CPU_ABI.length() % 10) + (Build.DEVICE.length() % 10) +
//(Build.DISPLAY.length() % 10) + (Build.HOST.length() % 10) + (Build.ID.length() % 10) + (Build.MANUFACTURER.length() % 10) +
//(Build.MODEL.length() % 10) + (Build.PRODUCT.length() % 10) + (Build.TAGS.length() % 10) + (Build.TYPE.length() % 10) +
//(Build.USER.length() % 10) + "default"
// But i didn't manage to generate the same value, may have to do with the api level of my test app. It can be extracted by sniffing the app requests cause for some reason they need to have it in their servers
const string DEVICE_KEY = "your device key";
static string STR_IV = "gcQu4mcDjQkPjcX1YY2X6xNaWyiWF0dUNbA";
//doesn't seem to be needed
const string EMPTY_KEY = "gcQu4mcDjQkPjcX1YY2X6xNaWyiWF0dD";
static int s(string d) => (d.Length % 10);
static byte[] KEY;
public static void InitializeEncryptedFields()
{
SHA256 sha = SHA256.Create();
KEY = sha.ComputeHash(Encoding.UTF8.GetBytes(DEVICE_KEY));
var aes = new RijndaelManaged();
aes.BlockSize = 128;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.IV = Encoding.GetEncoding("ISO-8859-1").GetBytes(STR_IV.Substring(0, 16));
aes.Key = KEY;
string DecryptB64Value(string value)
{
var data = Convert.FromBase64String(value);
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
using (MemoryStream msDecrypt = new MemoryStream(data))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
return srDecrypt.ReadToEnd();
}
}
}
}
WebReqHelper.TOKEN = (DecryptB64Value(TOKEN));
WebReqHelper.TOKEN_SECRET = (DecryptB64Value(TOKEN_SECRET));
}
}
class Program
{
const string ACCOUNT_USER_ID = "Your user ID";
static void Main(string[] args)
{
KeyInitialization.InitializeEncryptedFields();
//The arg order for requests matters as it's used to calculate the hmac signature
/*
Get registered cameras:
Dictionary<string, string> GetDevices = new Dictionary<string, string>();
GetDevices.Add("seq", "1");
GetDevices.Add(Constants.KEY_USERID, ACCOUNT_USER_ID);
var req = WebReqHelper.MakeRequest("/v4/devices/list", GetDevices);
*/
/*
Get recording list:
Dictionary<string, string> GetRecordings = new Dictionary<string, string>();
GetRecordings.Add("seq", "0");
GetRecordings.Add(Constants.KEY_USERID, ACCOUNT_USER_ID);
GetRecordings.Add("type", "");
GetRecordings.Add("sub_type", "");
GetRecordings.Add("from", "unix date");
GetRecordings.Add("to", "unix date");
GetRecordings.Add("limit", "100");
GetRecordings.Add("fromDB", "true");
GetRecordings.Add("expires", "10");
var req = WebReqHelper.MakeRequest("/v2/alert/list", GetRecordings);
*/
StreamReader t = new StreamReader(req.GetResponse().GetResponseStream());
Console.WriteLine(FormatJson(t.ReadToEnd()));
/*
//Decrypt stuff downloaded from their servers:
byte[] data = File.ReadAllBytes("F:/enc.jpg");
byte[] dec = crypt_file.decryptFile(data, "key (usually is returned by the same request that gives you the encrypted file)");
File.WriteAllBytes("F:/dec.jpg",dec);
*/
}
//https://stackoverflow.com/questions/4580397/json-formatter-in-c
private const string INDENT_STRING = " ";
static string FormatJson(string json)
{
int indentation = 0;
int quoteCount = 0;
var result =
from ch in json
let quotes = ch == '"' ? quoteCount++ : quoteCount
let lineBreak = ch == ',' && quotes % 2 == 0 ? ch + Environment.NewLine + String.Concat(Enumerable.Repeat(INDENT_STRING, indentation)) : null
let openChar = ch == '{' || ch == '[' ? ch + Environment.NewLine + String.Concat(Enumerable.Repeat(INDENT_STRING, ++indentation)) : ch.ToString()
let closeChar = ch == '}' || ch == ']' ? Environment.NewLine + String.Concat(Enumerable.Repeat(INDENT_STRING, --indentation)) + ch : ch.ToString()
select lineBreak == null
? openChar.Length > 1
? openChar
: closeChar
: lineBreak;
return String.Concat(result);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment