Verschlüsselung auf iOS mit Objective-C

info

date: 2013-10-02 17:37:08

tags: RSA Objective-C iOS

category: Objective-C

Created by: Stephan Bösebeck

logged in

ADMIN


Verschlüsselung auf iOS mit Objective-C

no english version available yet

Im Zuge der ganzen NSA/Prism/Snowden Diskussion und auch weil es mich interessiert, hab ich mich mal rangesetzt und mir ein paar Gedanken zum Thema "privacy & Security" im Internet gemacht.

Was momentan fehlt ist ein Weg, wirklich sicher zu kommunizieren. Die DE-Mail ist ja nur ein besserer Witz (sicherheitstechnisch zumindest) und GPG / PGP ist viel zu kompliziert zu benutzen und vor allem - man benötigt eine Identitätsprüfung, "komische" Zertifikate und anderes, was der Otto-Normal-User so nicht wirklich versteht.

Das ist alles viel zu weit davon entfernt, wirklich brauchbar zu sein oder auch nur annähernd Email den Rang abzulaufen. Eigentlich müsste es vollkommen transparent laufen und alles Verschlüsselt werden - von End-To-End. D.h. von einem Client werden die Daten so verschlüsselt, dass nur der Empfänger diese Daten entschlüsseln kann.

Im Zuge dessen habe ich versucht, auf IOS eine Public-Key-Verschlüsselung hin zu bekommen, um zu sehen, was man damit noch so anstellen kann. Das ganze hat mich doch vor mehr Probleme gestellt, als gedacht.

Zunächst mal ein Symmetrisches Verschlüsselungsverfahren - auch wenn das für das Ziel einer End-To-End-Verschlüsselung nicht wirklich ausreichend ist (symmetrische Schlüssel kann man immer per Brute-Force angreifen), so ist es doch wichtig, dass die Keys nicht irgendwo unverschlüsselt rumliegen, sondern, wenn überhaupt, dann mit einem guten Passwort verschlüsselt werden.

AES in Objective-C

Das war erstaunlicherweise gar nicht so kompliziert, es gibt da wirklich eine Menge Beispiele im Netzt zu, die auch wirklich funktionieren. Hier der Code zum Verschlüsseln von NSData:

[codesyntax lang="objc"]

- (NSData *)AES256EncryptWithKey:(NSString *)key{
    // 'key' should be 32 bytes for AES256, will be null-padded otherwise
    char keyPtr[kCCKeySizeAES256 + 1]; // room for terminator (unused)
    bzero( keyPtr, sizeof( keyPtr ) ); // fill with zeroes (for padding)

    // fetch key data
    [key getCString:keyPtr maxLength:sizeof( keyPtr ) encoding:NSUTF8StringEncoding];

    NSUInteger dataLength = [self length];

    //See the doc: For block ciphers, the output size will always be less than or
    //equal to the input size plus the size of one block.
    //That's why we need to add the size of one block here
    size_t bufferSize = dataLength + kCCBlockSizeAES128;
    void *buffer = malloc( bufferSize );

    size_t numBytesEncrypted = 0;
    CCCryptorStatus cryptStatus = CCCrypt( kCCEncrypt, kCCAlgorithmAES128, kCCOptionPKCS7Padding,
                                          keyPtr, kCCKeySizeAES256,
                                          NULL /* initialization vector (optional) */,
                                          [self bytes], dataLength, /* input */
                                          buffer, bufferSize, /* output */
                                          &numBytesEncrypted );
    if( cryptStatus == kCCSuccess )
    {
        //the returned NSData takes ownership of the buffer and will free it on deallocation
        return [NSData dataWithBytesNoCopy:buffer length:numBytesEncrypted];
    }

    free( buffer ); //free the buffer
    return nil;
}

[/codesyntax]

Das funktioniert tadellos und ist einfach zu verwenden. Da man symmetrisch verschlüsselt, muss man auch nicht mit 2 Keys rumhantieren und die evtl. sogar im KeyStore ablegen.

Die Entschlüsselung ist auch recht simpel:

[codesyntax lang="objc"]

- (NSData *)AES256DecryptWithKey:(NSString *)key{

    // 'key' should be 32 bytes for AES256, will be null-padded otherwise
    char keyPtr[kCCKeySizeAES256+1]; // room for terminator (unused)
    bzero( keyPtr, sizeof( keyPtr ) ); // fill with zeroes (for padding)

    // fetch key data
    [key getCString:keyPtr maxLength:sizeof( keyPtr ) encoding:NSUTF8StringEncoding];

    NSUInteger dataLength = [self length];

    //See the doc: For block ciphers, the output size will always be less than or
    //equal to the input size plus the size of one block.
    //That's why we need to add the size of one block here
    size_t bufferSize = dataLength + kCCBlockSizeAES128;
    void *buffer = malloc( bufferSize );

    size_t numBytesDecrypted = 0;
    CCCryptorStatus cryptStatus = CCCrypt( kCCDecrypt, kCCAlgorithmAES128, kCCOptionPKCS7Padding,
                                          keyPtr, kCCKeySizeAES256,
                                          NULL /* initialization vector (optional) */,
                                          [self bytes], dataLength, /* input */
                                          buffer, bufferSize, /* output */
                                          &numBytesDecrypted );

    if( cryptStatus == kCCSuccess )
    {
        NSLog(@"Decrypt success");
        //the returned NSData takes ownership of the buffer and will free it on deallocation
        return [NSData dataWithBytesNoCopy:buffer length:numBytesDecrypted];
    }

    free( buffer ); //free the buffer
    return nil;
}

[/codesyntax]

RSA Implementierung

Standardweg mit Bordmitteln

Zunächst hab ich mal die Bordmittel von iOS 6 (und später 7) benutzt, um daten Verschlüsseln zu können. Das hat zunächst auch recht gut funktioniert - hier ein code-Beispiel:

[codesyntax lang="objc" lines="fancy" lines_start="1"]

NSData *wrappedSymmetricKey = data;
SecKeyRef key = yes ? self.publicKeyRef : self.privateKeyRef;

size_t cipherBufferSize = SecKeyGetBlockSize(key);
size_t keyBufferSize = [wrappedSymmetricKey length];

NSMutableData *bits = [NSMutableData dataWithLength:keyBufferSize];
OSStatus sanityCheck = SecKeyDecrypt(key,kSecPaddingPKCS1,
(const uint8_t *) [wrappedSymmetricKey bytes],cipherBufferSize,[bits mutableBytes],&keyBufferSize);
NSAssert(sanityCheck == noErr, @"Error decrypting, OSStatus == %ld.", sanityCheck);

[bits setLength:keyBufferSize];

return bits;

[/codesyntax]

 

Das ist der Code, der einen gegebenen Datenblock (NSData*) verschlüsselt und die verschlüsselten Daten wiederum als NSData zurückgibt. (die Basis für diesen Code findet sich hier). Das funktioniert so weit auch wunderbar.... solange man bei jedem Start der Application, das Schlüsselpaar neu generiert.

Aber eins nach dem anderen. Was macht der Code? Er nutzt die auch unter OSX bekannte "Schlüsselbundverwaltung" bzw. dessen Pendant von iOS. Dort müssen die Schlüsselpaare abgelegt werden um sie entsprechend nutzen zu können.

Die Erstellung und speicherung eines Schlüsselpaares geht z.B. so (auch wieder von hier):

[codesyntax lang="objc"]

OSStatus sanityCheck = noErr;
    publicKeyRef = NULL;
    privateKeyRef = NULL;

    // First delete current keys.
    [self deleteAsymmetricKeys];

    // Container dictionaries.
    NSMutableDictionary *privateKeyAttr = [NSMutableDictionary dictionaryWithCapacity:0];
    NSMutableDictionary *publicKeyAttr = [NSMutableDictionary dictionaryWithCapacity:0];
    NSMutableDictionary *keyPairAttr = [NSMutableDictionary dictionaryWithCapacity:0];

    // Set top level dictionary for the keypair.
    [keyPairAttr setObject:(__bridge id) kSecAttrKeyTypeRSA forKey:(__bridge id) kSecAttrKeyType];
    [keyPairAttr setObject:[NSNumber numberWithUnsignedInteger:kSecAttrKeySizeInBitsLength] forKey:(__bridge id) kSecAttrKeySizeInBits];

    // Set the private key dictionary.
    [privateKeyAttr setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id) kSecAttrIsPermanent];
    [privateKeyAttr setObject:privateTag forKey:(__bridge id) kSecAttrApplicationTag];
    // See SecKey.h to set other flag values.

    // Set the public key dictionary.
    [publicKeyAttr setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id) kSecAttrIsPermanent];
    [publicKeyAttr setObject:publicTag forKey:(__bridge id) kSecAttrApplicationTag];
    // See SecKey.h to set other flag values.

    // Set attributes to top level dictionary.
    [keyPairAttr setObject:privateKeyAttr forKey:(__bridge id) kSecPrivateKeyAttrs];
    [keyPairAttr setObject:publicKeyAttr forKey:(__bridge id) kSecPublicKeyAttrs];

    // SecKeyGeneratePair returns the SecKeyRefs just for educational purposes.
    sanityCheck = SecKeyGeneratePair((__bridge CFDictionaryRef) keyPairAttr, &publicKeyRef, &privateKeyRef);

    if (publicKeyRef == NULL) {
        NSLog(@"did not get keys");
    }
    LOGGING_FACILITY( sanityCheck == noErr && publicKeyRef != NULL && privateKeyRef != NULL, @"Something really bad went wrong with generating the key pair." );

[/codesyntax]

 

Dabei wird ein neues Schlüsselpaar generiert und die Referenzen darauf lokal abgelegt (Variablen publicKeyRef und privateKeyRef).

So weit so gut, das klappt wunderbar, hat nur einige Einschränkungen: so kann man keine Schlüssel erzeugen, die größer sind als 4096 bit, außerdem müssen an stelle von dem Schlüssel selbst ja auch noch einige Zusatzinformationen abgespeichert werden, wie z.B. unter welchem "Namen" die keys abgelegt werden etc.

Das, was mich am meisten daran gestört hat, ist aber, dass es eine sehr unhandliche Schnittstelle ist, wo zwischen Objective-C und C/C++ hin und hergemapped werden muss. (Anm. ich hab die letzten Jahr(zehnt)e hauptsächlich Java programmiert, da ist alles etwas "sauberer"). Eine schönere Objective-C implementierung hab ich nicht gefunden. (falls ihr eine kennt, immer her damit...)

Aber was mich wirklich genervt hat, ist, dass ich es ums verrecken nicht hin bekommen habe, diese dämlichen RSA-Keys z.B. aus den UserDefaults wieder zu lesen. No chance... man kann sich die Bits zwar holen, kann aus den Bits auch wieder Keys erstellen lassen... allerdings landen die dann irgendwo im Storage und ich bekomme den Key nicht mehr da raus. Entweder er ist nil oder er kann nicht zum entschlüsseln benutzt werden.

Also, hier der letzte Code-Stand, evtl. habt ihr ja ne Idee, was da klemmen kann:

[codesyntax lang="objc"]

 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSData *privKey = [defaults dataForKey:@"privateKey"];
    NSData *pubKey = [defaults dataForKey:@"publicKey"];
    RSA *rsa = [RSA shareInstance];
    [rsa deleteAsymmetricKeys];
    if (privKey == nil) {
        NSLog(@"No key stored");

        NSLog(@"Generating new keys... please wait");
        self.aesPwdTF.text = @"generating keys...please wait";
        [self.textTF setEnabled:false];
        [self.aesPwdTF setEnabled:false];
        [rsa generateKeyPairRSACompleteBlock:^{
            NSLog(@"Keys prepared");
            self.aesPwdTF.text = @"";
            [self.textTF setEnabled:true];
            [self.aesPwdTF setEnabled:true];
            [self.aesPwdTF becomeFirstResponder];
            [defaults setObject:[rsa privateKeyBits] forKey:@"privateKey"];
            [defaults setObject:[rsa publicKeyBits] forKey:@"publicKey"];
        }];
    } else {
        [rsa setPrivateKey:privKey];
        [rsa setPublicKey:pubKey];
        self.publicKeyTF.text = [[rsa publicKeyBits] base64EncodedString];
        NSLog(@"Private key %@", [[rsa privateKeyBits] base64EncodedString]);
        NSLog(@"read keys from defaults");
    }

[/codesyntax]

 

Das ist der Teil, der entscheided, ob der Key noch mal eingelesen werden soll. Klar, man könnte das auch im Keystore drin lassen, aber leider kann ich die daten ja nicht auslesen, denn hier klemmt es jedes Mal in der einen oder anderen Form:

[codesyntax lang="objc"]

- (void)setPrivateKey:(NSData *)privateKey {
    privateKeyRef = NULL;
    OSStatus sanityCheck = noErr;
    SecKeyRef peerKeyRef = NULL;

    LOGGING_FACILITY( privateKey != nil, @"Private key parameter is nil." );

    NSMutableDictionary *peerPrivateKeyAttr = [[NSMutableDictionary alloc] init];

    [peerPrivateKeyAttr setObject:(__bridge id) kSecClassKey forKey:(__bridge id) kSecClass];
    [peerPrivateKeyAttr setObject:(__bridge id) kSecAttrKeyTypeRSA forKey:(__bridge id) kSecAttrKeyType];
    [peerPrivateKeyAttr setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id)kSecAttrIsPermanent];
    [peerPrivateKeyAttr setObject:privateTag forKey:(__bridge id) kSecAttrApplicationTag];
    [peerPrivateKeyAttr setObject:privateKey forKey:(__bridge id) kSecValueData];
    [peerPrivateKeyAttr setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id) kSecReturnPersistentRef];

    sanityCheck = SecItemAdd((__bridge CFDictionaryRef) peerPrivateKeyAttr, (CFTypeRef *) &peerKeyRef);
    if (sanityCheck) {
        if (sanityCheck != errSecDuplicateItem)
            return;
        // Already have a key with this digest, so look it up to get its ref:
        [peerPrivateKeyAttr removeObjectForKey: (__bridge id)kSecValueData];
        [peerPrivateKeyAttr setObject: privateTag forKey: (__bridge id)kSecAttrApplicationLabel];//??
        [peerPrivateKeyAttr removeObjectForKey: (__bridge id)kSecReturnPersistentRef];
        [peerPrivateKeyAttr setObject: (__bridge id)kCFBooleanTrue forKey: (__bridge id)kSecReturnPersistentRef];
        SecItemCopyMatching((__bridge CFDictionaryRef)peerPrivateKeyAttr, (CFTypeRef*)peerKeyRef);
    }

    NSMutableDictionary *queryPrivateKey = [[NSMutableDictionary alloc] init];

    // Set the private key query dictionary.
    [queryPrivateKey setObject:(__bridge id)kSecClassKey forKey:(__bridge id)kSecClass];
    [queryPrivateKey setObject:privateTag forKey:(__bridge id)kSecAttrApplicationTag];
    [queryPrivateKey setObject:(__bridge id)kSecAttrKeyTypeRSA forKey:(__bridge id)kSecAttrKeyType];
    [queryPrivateKey setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id)kSecReturnRef];
    SecItemCopyMatching
    ((__bridge CFDictionaryRef)queryPrivateKey, (CFTypeRef *)&peerKeyRef);
    privateKeyRef = peerKeyRef;

}

[/codesyntax]

Das blöde ist, dass nach der Ausführung dieses Codes, sowohl im Simulator als auch auf dem Gerät die Variable privateKeyRef entweder nil ist, oder nicht für die Entschlüsselung verwendet werden kann.

Aber ich wollte ja eh eine "schönere" Schnittstelle bauen, die das ganze etwas entmystifiziert...

You don't own it, 'till you make it

Also, wie funktioniert eigentlich RSA. So kompliziert ist das eigentlich gar nicht, allerdings handelt es sich dabei um sehr große Primzahlen. Hier eine Beispielimplementierung in Java:

[codesyntax lang="objc"]

public class RSA {
   private final static BigInteger one      = new BigInteger("1");
   private final static SecureRandom random = new SecureRandom();

   private BigInteger privateKey;
   private BigInteger publicKey;
   private BigInteger modulus;

   // generate an N-bit (roughly) public and private key
   RSA(int N) {
      BigInteger p = BigInteger.probablePrime(N/2, random);
      BigInteger q = BigInteger.probablePrime(N/2, random);
      BigInteger phi = (p.subtract(one)).multiply(q.subtract(one));

      modulus    = p.multiply(q);
      publicKey  = new BigInteger("65537");     // common value in practice = 2^16 + 1
      privateKey = publicKey.modInverse(phi);
   }

   BigInteger encrypt(BigInteger message) {
      return message.modPow(publicKey, modulus);
   }

   BigInteger decrypt(BigInteger encrypted) {
      return encrypted.modPow(privateKey, modulus);
   }

   public String toString() {
      String s = "";
      s += "public  = " + publicKey  + "n";
      s += "private = " + privateKey + "n";
      s += "modulus = " + modulus;
      return s;
   }

   public static void main(String[] args) {
      int N = Integer.parseInt(args[0]);
      RSA key = new RSA(N);
      System.out.println(key);

      // create random message, encrypt and decrypt
      BigInteger message = new BigInteger(N-1, random);

      //// create message by converting string to integer
      // String s = "test";
      // byte[] bytes = s.getBytes();
      // BigInteger message = new BigInteger(s);

      BigInteger encrypt = key.encrypt(message);
      BigInteger decrypt = key.decrypt(encrypt);
      System.out.println("message   = " + message);
      System.out.println("encrpyted = " + encrypt);
      System.out.println("decrypted = " + decrypt);
   }
}

[/codesyntax]

(Gefunden hier)

Eigentlich ist es ja nur nötig, ein paar Primzahlen zu generieren, die man dann potenziert und den modulo berechnet, sowohl zum Ver- als auch Entschlüsseln.

Klingt eigentlich total simpel, aber die Herausforderung liegt darin, mit Zahlen zu rechnen, die mehrere 1000 bit lang sind - man muss also ne eigene Arithmetik abbilden.

Das kann beliebig komplex werden, weshalb ich mich bei meiner Implementierung an die GNU-Java BigInteger Implementierung gehalten habe und diese mehr oder minder auf Objective-C portiert habe (Source Code dafür ist hier).

Die Portierung ist nicht wirklich einfach gewesen, vor allem weil Java und Objective-C / C++ eine andere Vorstellung von primitiven Datentypen haben, aber zumindest läuft die BigInteger Arithmetik erst mal so weit.

Nachdem ich eine BigInteger Implementierung in Objective-C zur Verfügung hatte, ist die Verschlüsselung ähnlich simpel wie in java:

[codesyntax lang="objc"]

- (BigInteger *)encryptBigInteger:(BigInteger *)message {
    if (message.bitLength > self.bitLen) {
        NSLog(@"Encrypting impossible: Message is too long for key! Key is %d bits wide, message is %d", self.bitLen, message.bitLength);
        //splitting it? how?
        return nil;
    }
    return [message modPow:self.e modulo:self.n];
}

- (BigInteger *)decryptBigInteger:(BigInteger *)message {
    return [message modPow:self.d modulo:self.n];
}

[/codesyntax]

Wobei da sicherlich noch ein paar Optimierungen passieren könnten, wie z.B. das Splitten zu großer Eingaben in mehrere BigIntegers mit der richtigen Größe für die Verschlüsselung.

Ich denke, das Prinzip von RSA ist bekannt und kann so Anwendung finden. Ich werde hier sicherlich noch mehr Code posten der euch dann vielleicht auch ne Menge Arbeit erspart.

Was jetzt noch fehlt sind mehr oder minder 2 Dinge: Das umwandeln von NSData in eine Menge von BigIntegers sowie der zugehörige umgekehrte Weg. Wenn das funktioniert, kann man jeden BigInteger-Wert einzeln ver- bzw. entschlüsseln und kann somit beliebige Daten sicher übertragen oder ablegen.

Aber darüber ein andern mal mehr...

Warum das ganze?

Naja, zum einen hat es mich stark interessiert, mal so ein verschlüsselungsverfahren umzusetzen - und da ich in Objective-C keine passende Implementierung gefunden habe (einen Haufen Implementierungen in c/c++ ja, aber keine wirklich objektorientiert, die man als Java-Entwickler leichter versteht), lag es doch besonders Nahe, das gleich mal in Objective-C zu implementieren.

Eine Anwendung dafür schwebt mir auch schon vor, mal sehen, in wie weit dass dann wirklich seinen Weg in den App-Store findet... so "stay tuned" ;-)