Tweak DRM

From iPhone Development Wiki

Building tweak DRM means building technical measures to control access to using a tweak you made, especially to prevent access by people who were supposed to pay for it but didn't, aka pirates. (DRM stands for digital rights management.)

Making DRM-related mistakes is a reliable way to get a thread at the top of /r/jailbreak with upset commenters saying that they won't buy anything from you again. Here is advice to help you avoid that situation. See "advice for dealing with piracy" on the advice for new developers page for other bits of useful information.

Philosophical advice

Many developers choose to skip working on DRM for their paid tweaks and instead spend that time on making a great piece of software with lots of marketing and good support. Almost all DRM systems get cracked; many people who crack software have more free time than you do.

To read a wide variety of perspectives from users and developers about the pros and cons of DRM, check out "This community's thoughts on DRM?" from /r/jailbreak.

If you choose to implement DRM, you need to build it in a way that preserves access for legitimate users even if your DRM system fails. If your server goes down (and it'll inevitably go down), or if other mistakes happen, that must not prevent access to your product for people who have purchased it. saurik is willing to block purchases of Cydia Store products with faulty DRM, and the default repositories (especially BigBoss) also don't consider faulty DRM acceptable.

Some advice from saurik:

"DRM...increases the risk of your product receiving complaints, complaints that will translate into your vendor account no longer being trusted and your products being blocked from sale. This instead must be viewed as a tradeoff that you must consider."

"I do not consider hard DRM checks that happen during package scripts acceptable. For various reasons (related to how other packages that modify things related to network access are implemented) it cannot be assumed to be the case that Cydia still has working Internet access during the package installation phase (all packages are downloaded before this phase begins, which then happens as a semi-atomic unit)."

"However, the entire idea of having a DRM system that doesn't let the user use the product they just purchased unless your server is working at that very first instant is fundamentally flawed: even simple networking glitches or inevitable hardware failures lead to situations where users can't enable their purchases. You thereby really need some kind of "leniency" or "grace period" in your implementation to be acceptable."

"One common way of getting most of the way there is simply to implement a trial period for the product: the trial period tends to act as a buffer, decreasing the number of people who experience the failure (as most people who ever purchase at all actually purchase quite quickly and do not wait for the product to entirely expire). A more epic implementation involves a model where the system gives the user a few hours of grace."

Practical advice

A polite pop-up message displayed by a cracked version of FolderEnhancer.

It's reasonable to make pirated versions display a friendly message (as a pop-up, as a banner in settings, or similar) that explains that (1) it's a pirated tweak, and (2) please uninstall this pirated version and purchase the legitimate version from the authorized repository, with a link to the proper package page. If this message is polite and not very annoying, tweak crackers might not bother to patch it out.

It's fair to make pirated versions of your tweak not work, but keep in mind that if you do this without explaining why, this may confuse people into thinking the tweak doesn't work at all, even for paid users. (Some people do use piracy to "try before they buy".) Also, tweak crackers like to find ways to bypass this so that the pirated tweak will work.

Keep in mind that some people pirate without realizing that they're using pirated versions - for example, their friend suggested that they add a shady repository, and they ignored the pirate warning on the repository. This is part of why it's helpful to include a clear message explaining that it's pirated.

Not recommended (don't act like malware)

DRM should not act like malware. This is part of the general principle that tweaks should never act like malware. Some of the things you shouldn't do (and also shouldn't scare people by claiming or threatening to do them):

  • Don't take action without permission from users, even if the person has pirated your package - for example, don't modify people's /etc/hosts files or send tweets without permission. This kind of negative surprise makes people angry and reduces their trust in you (both pirates and legitimate users who have already purchased your package or were considering purchasing it), which doesn't make them want to support you by purchasing your package. Note also that social networks typically forbid posts sent without the user's consent, for instance see the Twitter API terms.
  • Don't do annoying things to the operating system that frustrate and confuse people, such as making the screen black after they install a pirated package, even if that's solvable by going into safe mode and uninstalling the package. This makes people scared and irritated - they'll be angry at you instead of wanting to support your work by purchasing the package.
  • Don't cause unrelated problems that make people think there's something wrong with their jailbreak, such as making SpringBoard randomly crash once in a while - pirates are just going to think that they need to restore and go back to normal iOS, instead of realizing that installing your specific package caused their problem. You want to encourage them to purchase your package, not make them think jailbroken devices are unreliable.
  • Don't cause harm to files or the operating system (such as damaging system files, intentionally causing bootloops, etc.) - this is bad and will get your package purchase-blocked by saurik and/or removed from default repositories.
  • Don't damage devices. If you intentionally "brick" or otherwise cause permanent damage to other people's devices (see TheiPhoneWiki on bricking), you will be permanently banned from the Cydia Store and the default repositories, as well as banned from /r/jailbreak, #theos, #iphonedev, and anything else we can think of.
  • Don't make life difficult for other tweak developers who may need to coordinate their work with your work. For example, don't disable debuggers or other tools developers use.

Technical advice

You can add some advice here about writing good DRM!

Cydia Store Integration and crack prevention might be helpful articles.

You can also ask experienced developers for advice on this. One suggestion:

I've used a few different lightweight forms of DRM that don't interfere with User Experience at all, most of which have been unsuccessful (only delaying the inevitable). But there are a few schemes that can be successfully implemented (and so far moderately unbeaten) with appropriate use cases of the tweak. Message me on twitter (@apocolipse269) or IRC (apocolipse on ircsaurik, freenode, chronic-dev, etc) and I can offer some advise. -Apocolipse

Here's a thread on /r/jailbreak with some examples from developers.

Types of DRM

File Check

This type of protection uses NSFileManager to check for a file usually unique to the system when your package is installed on a device. This relies on the fact that crackers usually change the package identifier since the behavior of APT is undefined when multiple repos have the same package with the same version (some repos also display a link to the original package). A protection like this is usually not the strongest against crackers as the only thing they would need to do is change your path as seen in the binary.

 /var/lib/dpkg/info/com.example.packagename.list     -->    /var/lib/dpkg/info///////////////////cydia.list

On the left is a classic example seen in countless binaries to protect the tweak. The one on the right is how a cracker would change the string, making the tweak check for Cydia rather than checking the files you had given it to check. Now an easy way to protect this file path from getting changed is a little term called obfuscation, in which you hide the string in the binary making it not editable. This type of protection even with obfuscation is still a bit too easy for a cracker to crack as the cracker could eventually find the hidden string. Alternatively, they can write a tweak to hook whatever method you use to return an affirmative response.

if ([[NSFileManager defaultManager] fileExistsAtPath:@"/var/lib/dpkg/info/com.example.packagename.list"])
{
    //Do if the file exists
}
else
{
    //Do if file doesn't exist
}

Dpkg Status Check

dpkg has a simple function where it will tell you the status of the package.

$ dpkg -s dpkg
Package: dpkg
Status: install ok installed
Priority: required
Section: Packaging
Installed-Size: 828
Maintainer: Jay Freeman (saurik) <[email protected]>
Architecture: iphoneos-arm
Version: 1.14.25-9
Depends: bash, bzip2, coreutils-bin, diffutils, findutils, gzip, lzma, ncurses, tar
Description: package maintainance tools from Debian
Name: Debian Packager
Homepage: http://wiki.debian.org/Teams/Dpkg

This is a sample of the status of the dpkg package. There are many ways you can implement this in your tweak - some developers end up directly going to the path of the status file and searching that, but as you would have already guessed, it could be cracked by simply changing the path of the file. Another method is to use a call like system() to run a command and getting its response, and than comparing with the string. This can also be cracked as it has to end up in memory as plain text eventually and passed to a standard library function that a cracker can always hook. Someone can change "dpkg -s packageName" to a custom command such as "craK -s packageName", which would output the data that the tweak is expecting.

Server Checks

This can be a more effective protection. You'll need to find a way to get the device's UDID from the device and to the server where the UDID will be checked with Cydia. Here is the php code that Cydia has provided to developers:

<?php
$vendor = "vendorID";
$secret_key = "secretKey";
$udid = $_GET['udid']; //the udid sent from device

function urlsafe_b64encode($string) {
    return str_replace(array('+','/','='), array('-','_',''), base64_encode($string));
}

function cydia_($url, $dict, $key) {
    ksort($dict);
    $query = http_build_query($dict);
    $response = file_get_contents("$url?$query&signature=" . urlsafe_b64encode(hash_hmac("sha1", $query, $key, true)));
    $values = array();
    parse_str($response, $values);
    return $values;
}

function cydia_check($vendor, $package, $version, $device, $host, $mode, $key) {
    return cydia_("https://cydia.saurik.com/api/check", array(
        'device' => $device,
        'package' => $package,
        'vendor' => $vendor,
        'version' => $version,
        'mode' => $mode,
        'host' => $host,
        'nonce' => uniqid(mt_rand(), true),
        'timestamp' => time()
    ), $key);
}

if (!isset($udid))
{
    header("Status: 403 Unique device identifier not provided");
}
else
{
    $response = cydia_check($vendor, 'com.example.packageName', 'PackageVersion', $udid, $_SERVER['REMOTE_ADDR'], 'local', $secret_key); //make sure this is correct
    
    if (!isset($response['state']) or $response['state'] != 'completed')
    {
        echo "UDID has not purchased such package and this string will be returned";
    }
    else
    {
        echo "UDID has purchased this and this string will be returned";
    }
}
?>

You can encrypt an unique string sent to the device and then check with the device to see if the string is what you had assumed it would be. Make sure not to make it too easy for someone cracking it to see what the string is you're checking with, or the response from the server is encrypted and only accessible by your tweak using .htaccess. Do keep in mind that this is security through obscurity and doesn't make it impossible.

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
    NSError* error = nil;
    NSData* data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:@"https://www.example.com/server/protection.php?udid=%@", UDID]] options:NSDataReadingUncached error:&error];

    if (error) {
        NSLog(@"%@", [error localizedDescription]); //device not connected to wifi, usually...
    } else {
        NSLog(@"Data has loaded successfully."); //data returned with your response from server, this is where you will be checking the return value
    }
});

It's always good to encrypt the URL string so it can't be edited by crackers even if they find out your returning response. Using SSL (HTTPS) is a good idea since the UDID should be considered private information, but don't assume this makes it impossible to perform a man-in-the-middle attack on the connection. A self-signed certificate authority (CA) can always be added to the device's keychain, allowing interposing any request simply by signing it with a certificate from the CA. Additionally, pinning the certificate doesn't help much as this is easily disabled.

To the Server check an addition can be a license, where the UDID used in lets say https://www.example.com/server/protection.php?udid=%@ is going to get stored in the license, then have the license get the UDID manually, which means to get the Serial Number, Wifi Mac Address and Bluetooth mac address, and compare it with the original udid that was used in the url. This method is used in Classic Folders, and the DRM gets the real UDID like this


 CFTypeRef platformSerialNumber = IORegistryEntryCreateCFProperty(platformExpertDevice,CFSTR("IOPlatformSerialNumber"), kCFAllocatorDefault, 0);
 CFTypeRef platformWifiMacAddr  = IOregistryEntryCreateCFProperty(platformExpertDevice,CFSTR("IOMACAddress"), kCFAllocatorDefault, 0);
 CFTypeRef platformBluetoothMac = IORegistryEntryCreateCFProperty(platformExpertDevice,CFSTR("local-mac-address"), kCFAllocatorDefault, 0);
 strcat(platformSerialNumber, platformWifiMacAddr);
 strcat(platformSerialNumber, platformBluetoothmac);
 CFTypeRef UDID = sha1(platformSerialNumber);
 if(strcmp(orig_UDID, UDID)
 {
    return 0;
 }
 else 
 {
    return -1;
 }