diff --git a/modules/juce_tracktion_marketplace/marketplace/juce_KeyFileGeneration.h b/modules/juce_tracktion_marketplace/marketplace/juce_KeyFileGeneration.h index aab19a9a6b..0178a4ad53 100644 --- a/modules/juce_tracktion_marketplace/marketplace/juce_KeyFileGeneration.h +++ b/modules/juce_tracktion_marketplace/marketplace/juce_KeyFileGeneration.h @@ -51,6 +51,22 @@ public: const String& machineNumbers, const RSAKey& privateKey); + /** Similar to the above key file generation method but with an expiry time. + You must supply a Time after which this key file should no longer be considered as active. + + N.B. when an app is unlocked with an expiring key file, OnlineUnlockStatus::isUnlocked will + still return false. You must then check OnlineUnlockStatus::getExpiryTime to see if this + expiring key file is still in date and act accordingly. + + @see OnlineUnlockStatus + */ + static String JUCE_CALLTYPE generateExpiringKeyFile (const String& appName, + const String& userEmail, + const String& userName, + const String& machineNumbers, + const Time expiryTime, + const RSAKey& privateKey); + //============================================================================== /** This is a simple implementation of a key-generator that you could easily wrap in a command-line main() function for use on your server. diff --git a/modules/juce_tracktion_marketplace/marketplace/juce_OnlineUnlockForm.cpp b/modules/juce_tracktion_marketplace/marketplace/juce_OnlineUnlockForm.cpp index 8ef5920bc0..9c6b43641b 100644 --- a/modules/juce_tracktion_marketplace/marketplace/juce_OnlineUnlockForm.cpp +++ b/modules/juce_tracktion_marketplace/marketplace/juce_OnlineUnlockForm.cpp @@ -207,12 +207,12 @@ void OnlineUnlockForm::resized() passwordBox.setInputRestrictions (64); passwordBox.setFont (font); - r.removeFromBottom (30); + r.removeFromBottom (20); emailBox.setBounds (r.removeFromBottom (boxHeight)); emailBox.setInputRestrictions (512); emailBox.setFont (font); - r.removeFromBottom (30); + r.removeFromBottom (20); message.setBounds (r); diff --git a/modules/juce_tracktion_marketplace/marketplace/juce_OnlineUnlockStatus.cpp b/modules/juce_tracktion_marketplace/marketplace/juce_OnlineUnlockStatus.cpp index 739da1f653..e7a7cfa384 100644 --- a/modules/juce_tracktion_marketplace/marketplace/juce_OnlineUnlockStatus.cpp +++ b/modules/juce_tracktion_marketplace/marketplace/juce_OnlineUnlockStatus.cpp @@ -29,6 +29,42 @@ struct KeyFileUtils { + static XmlElement createKeyFileContent (const String& appName, + const String& userEmail, + const String& userName, + const String& machineNumbers, + const String& machineNumbersAttributeName) + { + XmlElement xml ("key"); + + xml.setAttribute ("user", userName); + xml.setAttribute ("email", userEmail); + xml.setAttribute (machineNumbersAttributeName, machineNumbers); + xml.setAttribute ("app", appName); + xml.setAttribute ("date", String::toHexString (Time::getCurrentTime().toMilliseconds())); + + return xml; + } + + static String createKeyFileComment (const String& appName, + const String& userEmail, + const String& userName, + const String& machineNumbers) + { + String comment; + comment << "Keyfile for " << appName << newLine; + + if (userName.isNotEmpty()) + comment << "User: " << userName << newLine; + + comment << "Email: " << userEmail << newLine + << "Machine numbers: " << machineNumbers << newLine + << "Created: " << Time::getCurrentTime().toString (true, true); + + return comment; + } + + //============================================================================== static String encryptXML (const XmlElement& xml, RSAKey privateKey) { MemoryOutputStream text; @@ -93,10 +129,10 @@ struct KeyFileUtils return decryptXML (keyFileText.fromLastOccurrenceOf ("#", false, false).trim(), rsaPublicKey); } - static StringArray getMachineNumbers (XmlElement xml) + static StringArray getMachineNumbers (XmlElement xml, StringRef attributeName) { StringArray numbers; - numbers.addTokens (xml.getStringAttribute ("mach"), ",; ", StringRef()); + numbers.addTokens (xml.getStringAttribute (attributeName), ",; ", StringRef()); numbers.trim(); numbers.removeEmptyStrings(); return numbers; @@ -110,6 +146,9 @@ struct KeyFileUtils { String licensee, email, appID; StringArray machineNumbers; + + bool keyFileExpires; + Time expiryTime; }; static KeyFileData getDataFromKeyFile (XmlElement xml) @@ -119,7 +158,18 @@ struct KeyFileUtils data.licensee = getLicensee (xml); data.email = getEmail (xml); data.appID = getAppID (xml); - data.machineNumbers.addArray (getMachineNumbers (xml)); + + if (xml.hasAttribute ("expiryTime") && xml.hasAttribute ("expiring_mach")) + { + data.keyFileExpires = true; + data.machineNumbers.addArray (getMachineNumbers (xml, "expiring_mach")); + data.expiryTime = Time (xml.getStringAttribute ("expiryTime").getHexValue64()); + } + else + { + data.keyFileExpires = false; + data.machineNumbers.addArray (getMachineNumbers (xml, "mach")); + } return data; } @@ -127,6 +177,7 @@ struct KeyFileUtils //============================================================================== const char* OnlineUnlockStatus::unlockedProp = "u"; +const char* OnlineUnlockStatus::expiryTimeProp = "t"; static const char* stateTagName = "REG"; static const char* userNameProp = "user"; static const char* keyfileDataProp = "key"; @@ -183,11 +234,22 @@ void OnlineUnlockStatus::load() KeyFileUtils::KeyFileData data; data = KeyFileUtils::getDataFromKeyFile (KeyFileUtils::getXmlFromKeyFile (status[keyfileDataProp], getPublicKey())); - if (! doesProductIDMatch (data.appID)) - status.removeProperty (unlockedProp, nullptr); + if (data.keyFileExpires) + { + if (! doesProductIDMatch (data.appID)) + status.removeProperty (expiryTimeProp, nullptr); - if (! machineNumberAllowed (data.machineNumbers, localMachineNums)) - status.removeProperty (unlockedProp, nullptr); + if (! machineNumberAllowed (data.machineNumbers, localMachineNums)) + status.removeProperty (expiryTimeProp, nullptr); + } + else + { + if (! doesProductIDMatch (data.appID)) + status.removeProperty (unlockedProp, nullptr); + + if (! machineNumberAllowed (data.machineNumbers, localMachineNums)) + status.removeProperty (unlockedProp, nullptr); + } } void OnlineUnlockStatus::save() @@ -289,7 +351,7 @@ bool OnlineUnlockStatus::applyKeyFile (String keyFileContent) { setUserEmail (data.email); status.setProperty (keyfileDataProp, keyFileContent, nullptr); - status.removeProperty (unlockedProp, nullptr); + status.removeProperty (data.keyFileExpires ? expiryTimeProp : unlockedProp, nullptr); var actualResult (0), dummyResult (1.0); var v (machineNumberAllowed (data.machineNumbers, getLocalMachineIDs())); @@ -298,6 +360,14 @@ bool OnlineUnlockStatus::applyKeyFile (String keyFileContent) dummyResult.swapWith (v); jassert (! dummyResult); + if (data.keyFileExpires) + { + if ((! dummyResult) && actualResult) + status.setProperty (expiryTimeProp, data.expiryTime.toMilliseconds(), nullptr); + + return getExpiryTime().toMilliseconds() > 0; + } + if ((! dummyResult) && actualResult) status.setProperty (unlockedProp, actualResult, nullptr); @@ -395,23 +465,24 @@ String KeyGeneration::generateKeyFile (const String& appName, const String& machineNumbers, const RSAKey& privateKey) { - XmlElement xml ("key"); - - xml.setAttribute ("user", userName); - xml.setAttribute ("email", userEmail); - xml.setAttribute ("mach", machineNumbers); - xml.setAttribute ("app", appName); - xml.setAttribute ("date", String::toHexString (Time::getCurrentTime().toMilliseconds())); - - String comment; - comment << "Keyfile for " << appName << newLine; - - if (userName.isNotEmpty()) - comment << "User: " << userName << newLine; - - comment << "Email: " << userEmail << newLine - << "Machine numbers: " << machineNumbers << newLine - << "Created: " << Time::getCurrentTime().toString (true, true); + XmlElement xml (KeyFileUtils::createKeyFileContent (appName, userEmail, userName, machineNumbers, "mach")); + const String comment (KeyFileUtils::createKeyFileComment (appName, userEmail, userName, machineNumbers)); + + return KeyFileUtils::createKeyFile (comment, xml, privateKey); +} + +String KeyGeneration::generateExpiringKeyFile (const String& appName, + const String& userEmail, + const String& userName, + const String& machineNumbers, + const Time expiryTime, + const RSAKey& privateKey) +{ + XmlElement xml (KeyFileUtils::createKeyFileContent (appName, userEmail, userName, machineNumbers, "expiring_mach")); + xml.setAttribute ("expiryTime", String::toHexString (expiryTime.toMilliseconds())); + + String comment (KeyFileUtils::createKeyFileComment (appName, userEmail, userName, machineNumbers)); + comment << "Expires: " << expiryTime.toString (true, true); return KeyFileUtils::createKeyFile (comment, xml, privateKey); } diff --git a/modules/juce_tracktion_marketplace/marketplace/juce_OnlineUnlockStatus.h b/modules/juce_tracktion_marketplace/marketplace/juce_OnlineUnlockStatus.h index 2bf8193642..18efa9f397 100644 --- a/modules/juce_tracktion_marketplace/marketplace/juce_OnlineUnlockStatus.h +++ b/modules/juce_tracktion_marketplace/marketplace/juce_OnlineUnlockStatus.h @@ -126,7 +126,15 @@ public: changed by a cracker in order to unlock your app, so the more places you call this method, the more hassle it will be for them to find and crack them all. */ - inline var isUnlocked() const { return status[unlockedProp]; } + inline var isUnlocked() const { return status[unlockedProp]; } + + /** Returns the Time when the keyfile expires. + + If a the key file obtained has an expiry time, isUnlocked will return false and this + will return a non-zero time. The interpretation of this is up to your app but could + be used for subscription based models or trial periods. + */ + inline Time getExpiryTime() const { return Time (static_cast (status[expiryTimeProp])); } /** Optionally allows the app to provide the user's email address if it is known. @@ -244,6 +252,7 @@ private: UnlockResult handleFailedConnection(); static const char* unlockedProp; + static const char* expiryTimeProp; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OnlineUnlockStatus) };