Writing a plugin that extends the functionality of CINEMA 4D is often only the first step towards a finished product. At one point or another during the work on your plugin you will have to spend some thought a proper license system.
This second article shows the implementation of a custom license class that can be used to validate a given license key against the customer’s information.
Implementing a license
Here we go, now we implement our own license. The resulting class will be the used for the license validation.
Since it is easy to demonstrate, we will use the MD5 Message Digest algorithm for our license key. If you prefer to use an Encryption/Decryption solution after all, you will find that it’s not difficult to change things afterwards.
class MyLicense
Here is the class header with a minimum set of member functions we will need.
#include "c4d.h" #include "lib_sn.h" #include "c4d_md5.h" extern Bool valid; Bool IsDemo(); Bool IsCommandLine(); class MyLicense { public: String CreateKey(const String& serial) const; LONG CheckLicense(const String& key) const; };
This is basically all functionality we need: Generating license keys, and validating license keys.
#include lib_sn.h
This is a header from the CINEMA 4D API. It provides us with classes and defines that come in handy when dealing with licenses.
#include c4d_md5.h
This is the header for the MD5 digestion algorithm wrapper function. Those are described lather in this article.
The source code for the MD5 algorithm is free, you can find it on the web.
I mostly use the implementation of Aladdin Enterprises, 2002, by L. Peter Deutsch.
extern Bool valid
This external Bool value will be used to store the result of the license check, and make it available in PluginStart() in main.cpp.
CreateKey
Now to the implementation…
In this example, we are going to use the MD5 digestion algorithm to create the key.
First, let’s implement the key generation. We’ll also add more functions, to keep things easily extendable.
#define MYLICENSE_PREFIX String("MYPLUGIN") String MyLicense::CreateKey(const String& serial) const { // Generate key return GetMD5(MYLICENSE_PREFIX + serial.Right(5)).ToUpper(); }
MYLICENSE_PREFIX
This is just a simple string to identify the plugin. That way, you can use the same license generation code for different plugins: If you have a unique identifier string for each of your plugins, the license keys generated for those plugins will also be different.
serial.Right(5)
We only use the last 5 digits of the serial number. That way, we get around having to send out a new license if a user purchased your plugin with his temporary serials and then gets the final serial numbers from MAXON.
Now, the final string that goes into GetMD5 is something like MYPLUGIN12345.
GetMD5()
That’s not a standard function. It’s part of some wrapper that I wrote to handle the standard MD5 code (RFC 1321), which you can find very easily on the web.
Here’s the wrapper header file:
#ifndef _C4D_MD5_H_ #define _C4D_MD5_H_ #include "c4d.h" #include "md5.h" String GetMD5(const String& p); #endif
Here’s the implementation:
#include "c4d_md5.h" CHAR *ConvString(const String& s, STRINGENCODING encoding=(STRINGENCODING)STRINGENCODING_7BIT) { // Use STRINGENCODING_UTF8 as same default encoding as Python LONG len = s.GetCStringLen(encoding); CHAR *code = (CHAR*) GeAllocNC(len+1); if (!code) return NULL; s.GetCString(code, len+1, encoding); return code; } String GetMD5(const String& p) { DigestMD5 md5; String key; CHAR *c_str = NULL; CHAR *c_tmp = NULL; c_str = ConvString(p, STRINGENCODING_7BIT); if (!c_str) return String(); c_tmp = md5.get_md5hash(c_str, (unsigned long)strlen(str)); if (!c_tmp) { GeFree(str); return String(); } key = c_tmp; GeFree(c_str); GeFree(c_tmp); return key; }
The MD5 function calls may be named differently in the MD5 sources you find on the web, but it will basically always work the same. This wrapper code simply converts the input string to a plain 7-bit encoded ASCII string, sends it to the MD5 function, gets the result, casts it back into a C4D String, and returns it.
Now we have implemented CreateKey(). It takes a C4D serial number (actually, it would take any string) and returns a key that is quasi unique for each serial number and for each of your plugins. The final license key looks somehow like this: 7e716d0e702df0505fc72e2b89467910.
Of course, the key could have looked nicer. For example, we could insert dashes after every four characters: 7e71-6d0e-702d-f050-5fc7-2e2b-8946-7910. And we can also choose just some of those 4-char blocks: 7e71-702d-5fc7-8946. That key looks more appealing, not so much like a typical MD5; and there still is virtually no chance that two different sets of input data would ever generate the same key.
There is a lot of nice stuff you could do with the key, to make it look nicer or to include more information. For this example, we want to keep it simple, so we’ll just use the MD5 string as it comes out.
CheckLicense
Now, let’s implement the license validation. Since we are using a digestion algorithm, we can not decrypt the passed license key to check the information inside it; we have to create a new key from the user’s information and compare it to the passed key.
Bool valid; LONG MyLicense::CheckLicense(const String& key) const; { if (IsDemo() || IsCommandLine() || IsNet()) { valid = TRUE; return SN_OKAY; } valid = FALSE; if (!strLic.Content()) return SN_WRONGNUMBER; // Get license data of this C4D installation String sC4dserial; LONG lNrOfLics = 0; GetC4DSerial(sC4dserial, lNrOfLics); if (!sC4dserial.Content()) return SN_WRONGNUMBER; // Generate key for comparison String sNewKey = GenerateKey(sC4dserial); // Compare & return result Bool bValidKey = sKey.Compare(sNewKey) == 0; if (bValidKey) { valid = TRUE; return SN_OKAY; } return SN_WRONGNUMBER; }
Bool valid
This is, again, the external Bool that we declared in the header.
return values
The SN_… defines are included in lib_sn.h. Since we will use our MyLicense class in an SNHook plugin later, it is a good idea to work with these value defines.
GetC4DSerial
This is a simple function that will return the 11 digits of the CINEMA 4D serial number. If CINEMA 4D is using a license server, the function will return the server serial and the number of seats (seats that the user purchased; it’s currently not possible to find out how many seats are actually in use). If it’s just a standard single-seat license, the number will be 1.
Bool GetC4DSerial(String &serial, LONG& lNrOfLics) { SerialInfo si; // Try to get Multi License GeGetSerialInfo(SERIALINFO_MULTILICENSE, &si); if (si.nr.Content()) { // Multi license (Lic Server) lNrOfLics = si.nr.SubStr(3,3).ToLong(); } else { // Single License GeGetSerialInfo(SERIALINFO_CINEMA4D, &si); lNrOfLics = 1; } if (!si.nr.Content()) return FALSE; else serial = si.nr; return TRUE; }
Other helper functions
IsDemo, IsCommandLine, IsNet
As discussed in the first part of this article, I always make my plugins run without license in the CINEMA 4D Demo, as well as in NETrender environments and in the command line version.
The demo, because I think that’s the best idea for a demo version; and NETrender + Command line version, simply because I think it’s fair.
These functions simply return TRUE if the plugin is running in a CINEMA 4D DEMO or a Command line version.
Bool IsDemo() { SYSTEMINFO si = GeGetSystemInfo(); return (si & SYSTEMINFO_SAVABLEDEMO); } Bool IsCommandLine() { SYSTEMINFO si = GeGetSystemInfo(); return (si & SYSTEMINFO_COMMANDLINE); }
About Server licenses
Using the license server is not easy, especially if you don’t have a C4D license server for testing. It is also quite a big topic. If you are interested in learning more, I really recommend you to read this thread on PluginCafé:
License Server & SNHookClass on PluginCafé
Keith Young, the developer of the Riptide Pro plugin, does a wonderful job summing up the basics and specifics of implementation.
Conclusion
Now we have implemented a key generation algorithm, and a function that validates a user’s license key. That’s basically everything we need for now. The next step will be to integrate the license check into our plugin.