The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

492 lines
17KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2015 - ROLI Ltd.
  5. Permission is granted to use this software under the terms of either:
  6. a) the GPL v2 (or any later version)
  7. b) the Affero GPL v3
  8. Details of these licenses can be found at: www.gnu.org/licenses
  9. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  10. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  11. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. ------------------------------------------------------------------------------
  13. To release a closed-source product which uses JUCE, commercial licenses are
  14. available: visit www.juce.com for more information.
  15. ==============================================================================
  16. */
  17. /* Note: there's a bit of light obfuscation in this code, just to make things
  18. a bit more annoying for crackers who try to reverse-engineer your binaries, but
  19. nothing particularly foolproof.
  20. */
  21. struct KeyFileUtils
  22. {
  23. static XmlElement createKeyFileContent (const String& appName,
  24. const String& userEmail,
  25. const String& userName,
  26. const String& machineNumbers,
  27. const String& machineNumbersAttributeName)
  28. {
  29. XmlElement xml ("key");
  30. xml.setAttribute ("user", userName);
  31. xml.setAttribute ("email", userEmail);
  32. xml.setAttribute (machineNumbersAttributeName, machineNumbers);
  33. xml.setAttribute ("app", appName);
  34. xml.setAttribute ("date", String::toHexString (Time::getCurrentTime().toMilliseconds()));
  35. return xml;
  36. }
  37. static String createKeyFileComment (const String& appName,
  38. const String& userEmail,
  39. const String& userName,
  40. const String& machineNumbers)
  41. {
  42. String comment;
  43. comment << "Keyfile for " << appName << newLine;
  44. if (userName.isNotEmpty())
  45. comment << "User: " << userName << newLine;
  46. comment << "Email: " << userEmail << newLine
  47. << "Machine numbers: " << machineNumbers << newLine
  48. << "Created: " << Time::getCurrentTime().toString (true, true);
  49. return comment;
  50. }
  51. //==============================================================================
  52. static String encryptXML (const XmlElement& xml, RSAKey privateKey)
  53. {
  54. MemoryOutputStream text;
  55. text << xml.createDocument (StringRef(), true);
  56. BigInteger val;
  57. val.loadFromMemoryBlock (text.getMemoryBlock());
  58. privateKey.applyToValue (val);
  59. return val.toString (16);
  60. }
  61. static String createKeyFile (String comment,
  62. const XmlElement& xml,
  63. RSAKey rsaPrivateKey)
  64. {
  65. String asHex ("#" + encryptXML (xml, rsaPrivateKey));
  66. StringArray lines;
  67. lines.add (comment);
  68. lines.add (String());
  69. const int charsPerLine = 70;
  70. while (asHex.length() > 0)
  71. {
  72. lines.add (asHex.substring (0, charsPerLine));
  73. asHex = asHex.substring (charsPerLine);
  74. }
  75. lines.add (String());
  76. return lines.joinIntoString ("\r\n");
  77. }
  78. //==============================================================================
  79. static XmlElement decryptXML (String hexData, RSAKey rsaPublicKey)
  80. {
  81. BigInteger val;
  82. val.parseString (hexData, 16);
  83. RSAKey key (rsaPublicKey);
  84. jassert (key.isValid());
  85. ScopedPointer<XmlElement> xml;
  86. if (! val.isZero())
  87. {
  88. key.applyToValue (val);
  89. const MemoryBlock mb (val.toMemoryBlock());
  90. if (CharPointer_UTF8::isValidString (static_cast<const char*> (mb.getData()), (int) mb.getSize()))
  91. xml = XmlDocument::parse (mb.toString());
  92. }
  93. return xml != nullptr ? *xml : XmlElement("key");
  94. }
  95. static XmlElement getXmlFromKeyFile (String keyFileText, RSAKey rsaPublicKey)
  96. {
  97. return decryptXML (keyFileText.fromLastOccurrenceOf ("#", false, false).trim(), rsaPublicKey);
  98. }
  99. static StringArray getMachineNumbers (XmlElement xml, StringRef attributeName)
  100. {
  101. StringArray numbers;
  102. numbers.addTokens (xml.getStringAttribute (attributeName), ",; ", StringRef());
  103. numbers.trim();
  104. numbers.removeEmptyStrings();
  105. return numbers;
  106. }
  107. static String getLicensee (const XmlElement& xml) { return xml.getStringAttribute ("user"); }
  108. static String getEmail (const XmlElement& xml) { return xml.getStringAttribute ("email"); }
  109. static String getAppID (const XmlElement& xml) { return xml.getStringAttribute ("app"); }
  110. struct KeyFileData
  111. {
  112. String licensee, email, appID;
  113. StringArray machineNumbers;
  114. bool keyFileExpires;
  115. Time expiryTime;
  116. };
  117. static KeyFileData getDataFromKeyFile (XmlElement xml)
  118. {
  119. KeyFileData data;
  120. data.licensee = getLicensee (xml);
  121. data.email = getEmail (xml);
  122. data.appID = getAppID (xml);
  123. if (xml.hasAttribute ("expiryTime") && xml.hasAttribute ("expiring_mach"))
  124. {
  125. data.keyFileExpires = true;
  126. data.machineNumbers.addArray (getMachineNumbers (xml, "expiring_mach"));
  127. data.expiryTime = Time (xml.getStringAttribute ("expiryTime").getHexValue64());
  128. }
  129. else
  130. {
  131. data.keyFileExpires = false;
  132. data.machineNumbers.addArray (getMachineNumbers (xml, "mach"));
  133. }
  134. return data;
  135. }
  136. };
  137. //==============================================================================
  138. #if JUCE_MODULE_AVAILABLE_juce_data_structures
  139. const char* OnlineUnlockStatus::unlockedProp = "u";
  140. const char* OnlineUnlockStatus::expiryTimeProp = "t";
  141. static const char* stateTagName = "REG";
  142. static const char* userNameProp = "user";
  143. static const char* keyfileDataProp = "key";
  144. static var machineNumberAllowed (StringArray numbersFromKeyFile,
  145. StringArray localMachineNumbers)
  146. {
  147. var result;
  148. for (int i = 0; i < localMachineNumbers.size(); ++i)
  149. {
  150. String localNumber (localMachineNumbers[i].trim());
  151. if (localNumber.isNotEmpty())
  152. {
  153. for (int j = numbersFromKeyFile.size(); --j >= 0;)
  154. {
  155. var ok (localNumber.trim().equalsIgnoreCase (numbersFromKeyFile[j].trim()));
  156. result.swapWith (ok);
  157. if (result)
  158. break;
  159. }
  160. }
  161. }
  162. return result;
  163. }
  164. //==============================================================================
  165. OnlineUnlockStatus::OnlineUnlockStatus() : status (stateTagName)
  166. {
  167. }
  168. OnlineUnlockStatus::~OnlineUnlockStatus()
  169. {
  170. }
  171. void OnlineUnlockStatus::load()
  172. {
  173. MemoryBlock mb;
  174. mb.fromBase64Encoding (getState());
  175. if (mb.getSize() > 0)
  176. status = ValueTree::readFromGZIPData (mb.getData(), mb.getSize());
  177. else
  178. status = ValueTree (stateTagName);
  179. StringArray localMachineNums (getLocalMachineIDs());
  180. if (machineNumberAllowed (StringArray ("1234"), localMachineNums))
  181. status.removeProperty (unlockedProp, nullptr);
  182. KeyFileUtils::KeyFileData data;
  183. data = KeyFileUtils::getDataFromKeyFile (KeyFileUtils::getXmlFromKeyFile (status[keyfileDataProp], getPublicKey()));
  184. if (data.keyFileExpires)
  185. {
  186. if (! doesProductIDMatch (data.appID))
  187. status.removeProperty (expiryTimeProp, nullptr);
  188. if (! machineNumberAllowed (data.machineNumbers, localMachineNums))
  189. status.removeProperty (expiryTimeProp, nullptr);
  190. }
  191. else
  192. {
  193. if (! doesProductIDMatch (data.appID))
  194. status.removeProperty (unlockedProp, nullptr);
  195. if (! machineNumberAllowed (data.machineNumbers, localMachineNums))
  196. status.removeProperty (unlockedProp, nullptr);
  197. }
  198. }
  199. void OnlineUnlockStatus::save()
  200. {
  201. MemoryOutputStream mo;
  202. {
  203. GZIPCompressorOutputStream gzipStream (&mo, 9);
  204. status.writeToStream (gzipStream);
  205. }
  206. saveState (mo.getMemoryBlock().toBase64Encoding());
  207. }
  208. char OnlineUnlockStatus::MachineIDUtilities::getPlatformPrefix()
  209. {
  210. #if JUCE_MAC
  211. return 'M';
  212. #elif JUCE_WINDOWS
  213. return 'W';
  214. #elif JUCE_LINUX
  215. return 'L';
  216. #elif JUCE_IOS
  217. return 'I';
  218. #elif JUCE_ANDROID
  219. return 'A';
  220. #endif
  221. }
  222. String OnlineUnlockStatus::MachineIDUtilities::getEncodedIDString (const String& input)
  223. {
  224. const String platform (String::charToString (getPlatformPrefix()));
  225. return platform + MD5 ((input + "salt_1" + platform).toUTF8())
  226. .toHexString().substring (0, 9).toUpperCase();
  227. }
  228. bool OnlineUnlockStatus::MachineIDUtilities::addFileIDToList (StringArray& ids, const File& f)
  229. {
  230. if (uint64 num = f.getFileIdentifier())
  231. {
  232. ids.add (getEncodedIDString (String::toHexString ((int64) num)));
  233. return true;
  234. }
  235. return false;
  236. }
  237. void OnlineUnlockStatus::MachineIDUtilities::addMACAddressesToList (StringArray& ids)
  238. {
  239. Array<MACAddress> addresses;
  240. MACAddress::findAllAddresses (addresses);
  241. for (int i = 0; i < addresses.size(); ++i)
  242. ids.add (getEncodedIDString (addresses.getReference(i).toString()));
  243. }
  244. StringArray OnlineUnlockStatus::MachineIDUtilities::getLocalMachineIDs()
  245. {
  246. StringArray ids;
  247. // First choice for an ID number is a filesystem ID for the user's home
  248. // folder or windows directory.
  249. #if JUCE_WINDOWS
  250. MachineIDUtilities::addFileIDToList (ids, File::getSpecialLocation (File::windowsSystemDirectory));
  251. #else
  252. MachineIDUtilities::addFileIDToList (ids, File ("~"));
  253. #endif
  254. // ..if that fails, use the MAC addresses..
  255. if (ids.size() == 0)
  256. MachineIDUtilities::addMACAddressesToList (ids);
  257. jassert (ids.size() > 0); // failed to create any IDs!
  258. return ids;
  259. }
  260. StringArray OnlineUnlockStatus::getLocalMachineIDs()
  261. {
  262. return MachineIDUtilities::getLocalMachineIDs();
  263. }
  264. void OnlineUnlockStatus::setUserEmail (const String& usernameOrEmail)
  265. {
  266. status.setProperty (userNameProp, usernameOrEmail, nullptr);
  267. }
  268. String OnlineUnlockStatus::getUserEmail() const
  269. {
  270. return status[userNameProp].toString();
  271. }
  272. bool OnlineUnlockStatus::applyKeyFile (String keyFileContent)
  273. {
  274. KeyFileUtils::KeyFileData data;
  275. data = KeyFileUtils::getDataFromKeyFile (KeyFileUtils::getXmlFromKeyFile (keyFileContent, getPublicKey()));
  276. if (data.licensee.isNotEmpty() && data.email.isNotEmpty() && doesProductIDMatch (data.appID))
  277. {
  278. setUserEmail (data.email);
  279. status.setProperty (keyfileDataProp, keyFileContent, nullptr);
  280. status.removeProperty (data.keyFileExpires ? expiryTimeProp : unlockedProp, nullptr);
  281. var actualResult (0), dummyResult (1.0);
  282. var v (machineNumberAllowed (data.machineNumbers, getLocalMachineIDs()));
  283. actualResult.swapWith (v);
  284. v = machineNumberAllowed (StringArray ("01"), getLocalMachineIDs());
  285. dummyResult.swapWith (v);
  286. jassert (! dummyResult);
  287. if (data.keyFileExpires)
  288. {
  289. if ((! dummyResult) && actualResult)
  290. status.setProperty (expiryTimeProp, data.expiryTime.toMilliseconds(), nullptr);
  291. return getExpiryTime().toMilliseconds() > 0;
  292. }
  293. if ((! dummyResult) && actualResult)
  294. status.setProperty (unlockedProp, actualResult, nullptr);
  295. return isUnlocked();
  296. }
  297. return false;
  298. }
  299. static bool canConnectToWebsite (const URL& url)
  300. {
  301. ScopedPointer<InputStream> in (url.createInputStream (false, nullptr, nullptr, String(), 2000, nullptr));
  302. return in != nullptr;
  303. }
  304. static bool areMajorWebsitesAvailable()
  305. {
  306. const char* urlsToTry[] = { "http://google.com", "http://bing.com", "http://amazon.com",
  307. "https://google.com", "https://bing.com", "https://amazon.com", nullptr};
  308. for (const char** url = urlsToTry; *url != nullptr; ++url)
  309. if (canConnectToWebsite (URL (*url)))
  310. return true;
  311. return false;
  312. }
  313. OnlineUnlockStatus::UnlockResult OnlineUnlockStatus::handleXmlReply (XmlElement xml)
  314. {
  315. UnlockResult r;
  316. if (const XmlElement* keyNode = xml.getChildByName ("KEY"))
  317. {
  318. const String keyText (keyNode->getAllSubText().trim());
  319. r.succeeded = keyText.length() > 10 && applyKeyFile (keyText);
  320. }
  321. if (xml.hasTagName ("MESSAGE"))
  322. r.informativeMessage = xml.getStringAttribute ("message").trim();
  323. if (xml.hasTagName ("ERROR"))
  324. r.errorMessage = xml.getStringAttribute ("error").trim();
  325. if (xml.getStringAttribute ("url").isNotEmpty())
  326. r.urlToLaunch = xml.getStringAttribute ("url").trim();
  327. if (r.errorMessage.isEmpty() && r.informativeMessage.isEmpty() && r.urlToLaunch.isEmpty() && ! r.succeeded)
  328. r.errorMessage = TRANS ("Unexpected or corrupted reply from XYZ").replace ("XYZ", getWebsiteName()) + "...\n\n"
  329. + TRANS("Please try again in a few minutes, and contact us for support if this message appears again.");
  330. return r;
  331. }
  332. OnlineUnlockStatus::UnlockResult OnlineUnlockStatus::handleFailedConnection()
  333. {
  334. UnlockResult r;
  335. r.errorMessage = TRANS("Couldn't connect to XYZ").replace ("XYZ", getWebsiteName()) + "...\n\n";
  336. if (areMajorWebsitesAvailable())
  337. r.errorMessage << TRANS("Your internet connection seems to be OK, but our webserver "
  338. "didn't respond... This is most likely a temporary problem, so try "
  339. "again in a few minutes, but if it persists, please contact us for support!");
  340. else
  341. r.errorMessage << TRANS("No internet sites seem to be accessible from your computer.. Before trying again, "
  342. "please check that your network is working correctly, and make sure "
  343. "that any firewall/security software installed on your machine isn't "
  344. "blocking your web connection.");
  345. return r;
  346. }
  347. OnlineUnlockStatus::UnlockResult OnlineUnlockStatus::attemptWebserverUnlock (const String& email,
  348. const String& password)
  349. {
  350. // This method will block while it contacts the server, so you must run it on a background thread!
  351. jassert (! MessageManager::getInstance()->isThisTheMessageThread());
  352. String reply (readReplyFromWebserver (email, password));
  353. DBG ("Reply from server: " << reply);
  354. ScopedPointer<XmlElement> xml (XmlDocument::parse (reply));
  355. if (xml != nullptr)
  356. return handleXmlReply (*xml);
  357. return handleFailedConnection();
  358. }
  359. #endif // JUCE_MODULE_AVAILABLE_juce_data_structures
  360. //==============================================================================
  361. String KeyGeneration::generateKeyFile (const String& appName,
  362. const String& userEmail,
  363. const String& userName,
  364. const String& machineNumbers,
  365. const RSAKey& privateKey)
  366. {
  367. XmlElement xml (KeyFileUtils::createKeyFileContent (appName, userEmail, userName, machineNumbers, "mach"));
  368. const String comment (KeyFileUtils::createKeyFileComment (appName, userEmail, userName, machineNumbers));
  369. return KeyFileUtils::createKeyFile (comment, xml, privateKey);
  370. }
  371. String KeyGeneration::generateExpiringKeyFile (const String& appName,
  372. const String& userEmail,
  373. const String& userName,
  374. const String& machineNumbers,
  375. const Time expiryTime,
  376. const RSAKey& privateKey)
  377. {
  378. XmlElement xml (KeyFileUtils::createKeyFileContent (appName, userEmail, userName, machineNumbers, "expiring_mach"));
  379. xml.setAttribute ("expiryTime", String::toHexString (expiryTime.toMilliseconds()));
  380. String comment (KeyFileUtils::createKeyFileComment (appName, userEmail, userName, machineNumbers));
  381. comment << newLine << "Expires: " << expiryTime.toString (true, true);
  382. return KeyFileUtils::createKeyFile (comment, xml, privateKey);
  383. }