Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Geleitwort
Vorwort
1 Hello iPhone
2 Die Reise nach iOS
3 Sehen und anfassen
4 Alles unter Kontrolle
5 Daten, Tabellen und Controller
6 Models, Layer, Animationen
7 Programmieren, aber sicher
8 Datenserialisierung und Internetzugriff
9 Multimedia
10 Jahrmarkt der Nützlichkeiten
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
Apps programmieren für iPhone und iPad von Klaus M. Rodewig, Clemens Wagner
Das umfassende Handbuch
Buch: Apps programmieren für iPhone und iPad

Apps programmieren für iPhone und iPad
Rheinwerk Computing
1172 S., geb., mit DVD
49,90 Euro, ISBN 978-3-8362-2734-6
Pfeil 7 Programmieren, aber sicher
Pfeil 7.1 iOS und Hardware
Pfeil 7.2 Bedrohungen, Angriffe, Sicherheitslücken und Maßnahmen
Pfeil 7.2.1 Arten von Sicherheitslücken
Pfeil 7.3 Threat Modeling
Pfeil 7.3.1 Erstellen eines Datenflussdiagramms
Pfeil 7.3.2 STRIDE
Pfeil 7.3.3 Generische Designgrundsätze
Pfeil 7.3.4 Threat Modeling aus der Tube – das Microsoft SDL Threat Modeling Tool
Pfeil 7.4 Sichere Programmierung in der Praxis
Pfeil 7.4.1 Authentisierung
Pfeil 7.4.2 Keychain
Pfeil 7.4.3 Jailbreak-Erkennung
Pfeil 7.4.4 Verzeichnisse und Dateiattribute
Pfeil 7.4.5 Event-Handling
Pfeil 7.4.6 Screenshots
Pfeil 7.4.7 Sorgfältiger Umgang mit der Bildschirmsperre
Pfeil 7.4.8 Struktur und Ordnung im Sandkasten
Pfeil 7.4.9 UDID ist tot. Was nun?
Pfeil 7.4.10 Base64
Pfeil 7.5 iCloud
Pfeil 7.5.1 Denkanstöße
Pfeil 7.5.2 iCloud in der Praxis
Pfeil 7.5.3 Key-Value-Storage
Pfeil 7.5.4 Verschlüsselung (in der Cloud)

Rheinwerk Computing - Zum Seitenanfang

7.4Sichere Programmierung in der PraxisZur nächsten Überschrift

Auf die Sicherheit von iOS selbst können Sie als Programmierer keinen Einfluss nehmen. Sie können aber die vom Betriebssystem zur Verfügung gestellten Sicherheitsmechanismen nutzen, um Ihre App und die von ihr verarbeiteten und abgespeicherten Daten gegen unbefugten Zugriff zu sichern.

Als Beispiel für die Anwendung dieser Sicherheitsmechanismen dient das Fototagebuch aus Kapitel 5, »Daten, Tabellen und Controller«. Im Verzeichnis iOS7/SecurePhotoDiary im Git oder auf der Buch-DVD finden Sie das fertige, mit allen Sicherheitsmechanismen versehene Fototagebuch. Wenn Sie die Beispiele selbst nachprogrammieren möchten, nehmen Sie die Version aus Kapitel 5 (iOS6/PhotoDiary).


Rheinwerk Computing - Zum Seitenanfang

7.4.1AuthentisierungZur nächsten ÜberschriftZur vorigen Überschrift

Eine der elementarsten Gefahren, die einer App drohen, ist die Benutzung durch einen unbefugten Benutzer (Spoofing). Das kann eine arglose Ehefrau sein, die die Fotos von der Vertriebsparty in Budapest findet, ein pubertierender Jugendlicher, der nicht möchte, dass die Eltern oder Geschwister sein Fototagebuch lesen, etc.

Da das Fototagebuch personenbezogene und sensible Daten speichert, besteht also der erste Schritt darin, die App mit einer Authentisierungsfunktion zu versehen, so dass der Benutzer sich mit einem Passwort an der App anmelden muss, um sie verwenden zu können.

Der Charme einer eigenen Authentisierung innerhalb der App besteht darin, dass ein Benutzer keinen Code für sein iDevice vergeben muss, der unbefugte Zugriff auf die App aber trotzdem wirksam verhindert wird. Es ist zwar keine gute Idee, sein iDevice ohne Benutzercode zu verwenden (siehe Abschnitt 7.4.2, »Keychain«), aber als App-Programmierer können Sie nur für seine eigene App Sorge tragen, nicht für die Sicherheit des Gesamtsystems.

Um das Fototagebuch mit einem Authentisierungsmechanismus zu versehen, öffnen Sie das Projekt und fügen im Storyboard für das iPhone (Main.storyboard) zwei neue Viewcontroller samt neuer Klassendateien hinzu. Der eine Viewcontroller bekommt den Namen LoginViewController, den anderen nennen Sie RegistrationViewController. Weisen Sie beiden Viewcontrollern die betreffende Klasse zu (siehe Abbildung 7.20 und Abbildung 7.21).

Abbildung

Abbildung 7.20 Die Klasse für den »LoginViewController«

Abbildung

Abbildung 7.21 Die Klasse für den »RegistrationViewController«

Den LoginViewController konfigurieren Sie über die Checkbox Is Initial View Controller im Attributes Inspector als den Viewcontroller, der beim Start der App als erster angezeigt wird (siehe Abbildung 7.22). Der daraufhin im Storyboard erscheinende Pfeil zeigt die Änderung an.

Damit die App mit dem neuen Viewcontroller funktioniert, müssen Sie die Methode didFinishLaunchingWithOptions im AppDelegate (SecurePhotoDiaryAppDelegate.m) wie folgt anpassen:

self.managedObjectContext = [[NSManagedObjectContext alloc] 
init];
self.managedObjectContext.persistentStoreCoordinator =
self.storeCoordinator;
self.viewController = self.window.rootViewController;
if([self.viewController isKindOfClass:[UISplitViewController
class]]) {

UISplitViewController *theController = (UISplitViewController
*)self.viewController;
UINavigationController *theDetailController =
theController.viewControllers.lastObject;
theController.delegate = [theDetailController.viewControllers
objectAtIndex:0];
}
return YES;

Listing 7.8 Die neue Methode »didFinishLaunchingWithOptions«

Anschließend statten Sie beide Viewcontroller mit einigen GUI-Elementen aus. Der LoginViewController erhält ein Label (UILabel), ein Textfeld (UITextField) und einen Button (UIButton). Im Attributes Inspector setzen Sie das Textfeld auf Secure (siehe Abbildung 7.22). Dadurch maskiert iOS die in das Feld eingegebenen Zeichen und deaktiviert das Caching der eingegebenen Inhalte. Beides sind sehr sinnvolle Eigenschaften bei der Eingabe von Passwörtern.

Abbildung

Abbildung 7.22 Das »Secure«-Flag des »TextFields«

Arrangieren Sie Label, Textfeld und Button so auf dem LoginViewController, dass eine sinnvolle Anmeldemaske entsteht (siehe Abbildung 7.23).

Den RegistrationViewController dekorieren Sie mit Hilfe von zwei Labels, zwei Textfeldern und einem Button so wie in Abbildung 7.24 gezeigt.

Abbildung

Abbildung 7.23 Die Anmeldemaske der App

Abbildung

Abbildung 7.24 Die Registriermaske der App

Auch hier setzen Sie bei beiden Textfeldern das Secure-Flag. Anschließend geben Sie dem LoginViewController im Identity Inspector die Storyboard ID Login (siehe Abbildung 7.25).

Abbildung

Abbildung 7.25 Ein Identifier für den »LoginViewController«

Der Registrationviewcontroller erhält die STORYBOARD ID Registration (siehe Abbildung 7.26).

Abbildung

Abbildung 7.26 Ein Identifier für den »RegistrationViewController«

Der Sinn hinter diesem Arrangement sollte jetzt klar sein: Beim Start der App erscheint der LoginViewController und fragt das Benutzerkennwort ab, das der Benutzer für diese App vergeben hat. Ist keines vorhanden, erscheint der RegistrationViewController, über den der Benutzer ein Kennwort vergeben kann. Genau diese Funktionalität werden Sie im Folgenden implementieren.

Der erste Schritt ist die Implementierung der RegistrationViewController. Die App muss einen Benutzer beim ersten Start ja dazu auffordern, ein Passwort zu vergeben, und dieses Passwort speichern. Erzeugen Sie mit dem Assistant Editor zwei Outlets und eine Action in der Klasse RegistrationViewController:

#import <UIKit/UIKit.h>

@interface RegistrationViewController : UIViewController
- (IBAction)registerUser:(id)sender;
@property (weak, nonatomic) IBOutlet UITextField *firstPassword;
@property (weak, nonatomic) IBOutlet UITextField *secondPassword;
@end

Abbildung

Abbildung 7.27 Das erste Passwort-Feld

Abbildung

Abbildung 7.28 Das zweite Passwort-Feld folgt umgehend.

Die Action verbinden Sie mit dem Button auf dem RegistrationViewController, die Property firstPassword mit dem oberen Textfeld und die Property secondPassword mit dem unteren.

Abbildung

Abbildung 7.29 Der Button führt zur Action »registerUser«.

Implementieren Sie nun die Methode registerUser in der Datei RegisterViewController.m:

01 – (IBAction)registerUser:(id)sender {
02 NSString *password = [firstPassword text];
03 if([password isEqualToString:[secondPassword text]]){
04 NSLog(@"[+] Password accepted. Creating hash
for secure storage");
05 NSString *passwordHash = [SecUtils
generateSHA256:password];
06 NSLog(@"[+] Password hash: %@", passwordHash);
07 NSLog(@"[+] Password accepted. Writing to
Keychain");
08 if([SecUtils addKeychainEntry:passwordHash]){
09 [[NSUserDefaults standardUserDefaults]
setBool:YES forKey:@"passwordSet"];
10 [[NSUserDefaults standardUserDefaults]
synchronize];
11 UIStoryboard *storyboard = [UIStoryboard
storyboardWithName:@"MainStoryboard" bundle:nil];
12 UIViewController *vc = [storyboard
instantiateViewControllerWithIdentifier:@"Login"];
13 [vc
setModalPresentationStyle:UIModalPresentationFullScreen];
14 [self presentModalViewController:vc animated:YES];
15 }
16 } else {
17 UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:@"Passwörter stimmen nicht überein!"
message:@"Bitte erneut versuchen!"
delegate:self
cancelButtonTitle:@"Ok"
otherButtonTitles:nil];
18 [alert show];
19 UIStoryboard *storyboard = [UIStoryboard
storyboardWithName:@"MainStoryboard" bundle:nil];
20 UIViewController *vc = [storyboard
instantiateViewControllerWithIdentifier:@"Registration"];
21 [vc
setModalPresentationStyle:UIModalPresentationFullScreen];
22 [self presentModalViewController:vc animated:YES];
23 }
24 }

Listing 7.9 Die Methode »registerUser«

Die Methode nimmt den Inhalt des ersten Textfeldes (Zeile 2) und vergleicht ihn mit dem Inhalt des zweiten Textfeldes (Zeile 3). Damit wird überprüft, ob der Benutzer sein Passwort korrekt eingegeben hat. Stimmen die Passwörter nicht überein, springt die Methode zu Zeile 17 und zeigt dem Benutzer ein UIAlertView an, das ihn über die fehlerhafte Eingabe informiert. Anschließend ruft die Methode den Viewcontroller mit dem Identifier Registration auf. Den Identifier haben Sie beim Hinzufügen der Viewcontroller zum Storyboard vergeben, der Aufruf führt also zum erneuten Laden des RegistrationViewController. Neuer Versuch, neues Glück.

Stimmen beide Passwörter überein, übergibt die Methode das Passwort an die Methode generateSHA256 des Objekts SecUtils. Das Objekt SecUtils, dessen Klasse Sie im Anschluss implementieren werden, erzeugt eine sichere, speicherbare Form des Passworts, die registerUser im NSString passwordHash ablegt.

In Zeile 8 wird das vom Benutzer eingegebene Passwort an die Methode addKeychainEntry des Objekts SecUtils übergeben. Ist der Aufruf erfolgreich, was die if-Abfrage überprüft, wird ein Vermerk in NSUserDefaults geschrieben, dass ein Benutzerpasswort gespeichert worden ist. Dazu wird dem booleschen Schlüssel passwortSet der Wert YES zugewiesen.

Diese Information wird verwendet, um bei einem erneuten Aufruf der App nicht mehr den RegistrationViewController, sondern den LoginViewController anzuzeigen. Nach einmaliger erfolgreicher Registrierung soll der Benutzer ja nur noch sein Passwort eingeben müssen.

Die Methode synchronize von NSUserDefaults in Zeile 10 ist wichtig, um den neuen Eintrag persistent abzuspeichern. Ansonsten kann es passieren, dass der Benutzer sein Passwort zwar erfolgreich setzt, der Eintrag in den NSUserDefaults aber nicht dauerhaft gespeichert wird. Beim nächsten Start der App müsste der Benutzer sich dann erneut registrieren.

Abbildung

Abbildung 7.30 Das Outlet für das Passwort-Feld

Nach dem erfolgreichen Speichern des Passwortes wird der Viewcontroller mit dem Identifier Login aufgerufen: Diesen Identifier haben Sie oben vergeben, und es handelt sich dabei um den LoginViewController. Der Benutzer muss sich nach der erfolgreichen Vergabe des Passwortes also noch an der App anmelden.

Der LoginViewController erhält eine Action loginUser und ein Outlet password. Die Action verbinden Sie mit dem Button des LoginViewController, das Outlet mit dem Textfeld.

Abbildung

Abbildung 7.31 Die Action zum Login des Benutzers

Darüber hinaus erhält der LoginViewController eine Property passwordSet vom Typ BOOL. Das komplette Interface sieht dann wie folgt aus:

@interface LoginViewController ()
@property (weak, nonatomic) IBOutlet UITextField *password;
- (IBAction)loginUser:(id)sender;
@property BOOL passwordSet;
@end

Listing 7.10 Das Interface des »LoginViewController«

Da der LoginViewController der initiale Viewcontroller der App ist (Sie haben das eben im Storyboard so festgelegt), wird er nach dem Start der App als Erstes geladen. Er muss daher prüfen, ob der Benutzer sich bereits registriert hat, ob also der Schlüssel passwordSet in NSUserDefaults vorhanden und auf den Wert TRUE gesetzt ist. Dies erfolgt in der Methode viewDidAppear:

- (void)viewDidAppear:(BOOL)animated{
passwordSet = [[NSUserDefaults standardUserDefaults]
boolForKey:@"passwordSet"];
if(!passwordSet){
NSLog(@"Passwort nicht gesetzt");
NSLog(@"Starte Registrierung");
UIStoryboard *storyboard = [UIStoryboard
storyboardWithName:@"MainStoryboard" bundle:nil];
UIViewController *vc = [storyboard
instantiateViewControllerWithIdentifier:
@"Registration"];
[vc
setModalPresentationStyle:
xUIModalPresentationFullScreen];
[self presentModalViewController:vc animated:YES];
}
}

Listing 7.11 Die Methode »viewDidAppear« des »LoginViewController«

Ist der Wert passwortSet nicht vorhanden bzw. nicht auf TRUE gesetzt, hat der Benutzer noch kein Passwort gesetzt, und die Methode lädt den RegistrationViewController. Ist ein Passwort gesetzt, sieht der Benutzer das Textfeld und den Button und muss zur Anmeldung sein Passwort eingeben. Die Implementierung der Action loginUser ist die folgende:

- (IBAction)loginUser:(id)sender {
NSLog(@"[+] %@", NSStringFromSelector(_cmd));

NSString *storedPassword = [SecUtils
getUserPwFromKeychain];

NSLog(@"[+] PW: %@", storedPassword);

NSString *userPassword = [self.password text];

NSString *passwordHash = [SecUtils
generateSHA256:userPassword];
NSLog(@"[+] Password hash: %@", passwordHash);
NSLog(@"[+] Password: %@", userPassword);

if([passwordHash isEqualToString:storedPassword]){
UIStoryboard *storyboard = [UIStoryboard
storyboardWithName:@"MainStoryboard" bundle:nil];
UIViewController *vc = [storyboard
instantiateViewControllerWithIdentifier:
@"MainNavigationController"];
[vc
setModalPresentationStyle:UIModalPresentationFullScreen];
[self presentModalViewController:vc animated:YES];
} else {
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:@"Anmeldung fehlgeschlagen"
message:@"Bitte erneut versuchen!"
delegate:self
cancelButtonTitle:@"Ok"
otherButtonTitles:nil];
[alert show];
}
}

Listing 7.12 Die Methode »loginUser « im »LoginViewController«

Zu Beginn liest die Methode über den Aufruf getUserPwFromKeychain an SecUtils das in der Keychain gespeicherte Benutzerpasswort aus. Das vom Benutzer in das Textfeld eingegebene Passwort wird über den Methodenaufruf generateSHA256 an SecUtils in einen Hash umgewandelt (Details dazu folgen in der Erläuterung von SecUtils) und anschließend mit dem Passwort aus der Keychain verglichen. Wichtig ist dabei die Verwendung von isEqualToString, denn es sollen ja keine Objekte, sondern Zeichenketten verglichen werden.

Entspricht das vom Benutzer eingegebene Passwort dem in der Keychain gespeicherten, lädt der LoginViewController den MainNavigationController – die Hauptansicht des Fototagebuchs. Lassen Sie die NSLog-Anweisung ruhig vorerst in der Methode, an ihr werden wir später eine typische Sicherheitslücke veranschaulichen.

Der eigentlich spannende Teil der Authentisierungsfunktion liegt in der Klasse SecUtils. Diese Klasse erledigt die Kommunikation mit der Keychain und führt kryptografische Operationen für den sicheren Umgang mit dem Benutzerpasswort durch.

Das Interface sieht wie folgt aus:

01 #import <UIKit/UIKit.h>
02 #import <CommonCrypto/CommonDigest.h>
03 #define SALT
@"loremipsumdolorsitametconsectetueradipiscingelit"
04 #define KEYCHAIN_LABEL @"PhotoDiary"
05 #define KEYCHAIN_SERVICE @"Foo Services Ltd."
06 #define KEYCHAIN_ACCOUNT @"PhotoDiaryUser"
07 @interface SecUtils : NSObject
08 +(NSString *)generateSHA256:(NSString *)inputString;
09 +(BOOL)addKeychainEntry:(NSString *)entry;
10 +(NSString *)getUserPwFromKeychain;
11 @end

Listing 7.13 Das Interface der Klasse »SecUtils«

In Zeile 2 wird eine Headerdatei der Kryptografie-Bibliothek von iOS importiert. Diese Bibliothek, CommonCrypto, stellt verschiedene kryptografische Algorithmen und Werkzeuge zur Verfügung, mit denen sich Daten schützen lassen. Die importierte Headerdatei, CommonDigest.h, bietet den Zugriff auf verschiedene Hashfunktionen.

Zeile 3 definiert das Makro SALT und weist ihm eine lange Zeichenkette zu (siehe dazu die Top 25 im Absatz 7.2.1). Die Zeilen 4 bis 6 legen Werte fest, mit denen das Passwort in der Keychain abgelegt wird.

Die in den Zeilen 8 bis 10 deklarierten Methoden sind diejenigen, die von den beiden Viewcontrollern verwendet werden. Bei allen vier Methoden handelt es sich um Klassenmethoden. Da die Klasse SecUtils eine Sammlung von Methoden für den Umgang mit dem Benutzerpasswort ist und keine Daten speichern soll, erspart man sich durch die Verwendung von Klassenmethoden das Erzeugen eines Objekts, was nur unnötigen Speicherplatz verbrauchen würde.

Jetzt geht es an die Implementierung von SecUtils. Die interessanteste Methode ist generateSHA256, weil sie für die Sicherheit des Passworts verantwortlich ist:

01 +(NSString *)generateSHA256:(NSString *)inputString{
02 NSString *passwordWithSalt = [NSString
stringWithFormat:@"%@%@", SALT, inputString];
03 NSMutableString *passwordHash = [NSMutableString
stringWithCapacity:CC_SHA256_DIGEST_LENGTH];
04 unsigned char passwordChars[CC_SHA256_DIGEST_LENGTH];
05 CC_SHA256([passwordWithSalt UTF8String],
[ passwordWithSalt
06 lengthOfBytesUsingEncoding:NSUTF8StringEncoding],
passwordChars);
07 for(int i=0; i< CC_SHA256_DIGEST_LENGTH; i++){
08 [passwordHash appendString:[NSString
stringWithFormat:@"%02x", passwordChars[i]]];
09 }
10 return passwordHash;
11 }

Listing 7.14 Die Methode »generateSHA256«

Die Methode übernimmt in Zeile 1 als Eingabeparameter das Benutzerpasswort (inputString) als NSString. In Zeile 2 wird eine neue Zeichenkette erzeugt. Diese Zeichenkette besteht aus der in der Headerdatei definierten Konstanten SALT sowie dem Benutzerpasswort. Damit erklären sich auch Name und Sinn der Konstanten. SALT steht für den Fachbegriff Salt (bzw. in der deutschen Übersetzung Salz). Das Salz wird beim Erzeugen von Hashwerten aus Passwörtern verwendet, um die Passwörter um eine zufällige Zeichenkette zu erweitern, bevor sie durch die Hashfunktion laufen (siehe Top 25, »Verwendung von Hashfunktionen ohne Salz«).

Gelangt ein Angreifer in den Besitz von Hashwerten, kann er diese zwar mathematisch nicht in den ursprünglichen Klartext zurückwandeln, er kann aber einen Brute-Force-Angriff gegen den Hashwert durchführen. Dabei erzeugt er aus einer langen Liste von Passwörtern, einer sogenannten Wordlist, die entsprechenden Hashwerte. Mit dieser Liste von Hashwerten muss er dann nur die erbeuteten Hashwerte vergleichen. Findet er den passenden Hashwert in der Liste, kennt er das Passwort, aus dem der Hashwert erzeugt wurde.

Die mathematisch optimierte Form einer solchen Hash-Liste nennt sich Rainbow Table. Im Internet finden Sie Terabytes an vorausberechneten Rainbow Tables für die verschiedensten Hash-Algorithmen [Anm.: http://freerainbowtables.com] , was den Angriff auf erbeutete Hashwerte extrem einfach gestaltet.

Die Verwendung eines Salt-Wertes schützt vor diesen Angriffen über vorausberechnete Hashwerte. Das lässt sich einfach an einem Beispiel zeigen. Im Fototagebuch wird der Hash-Algorithmus SHA256 verwendet. Wenn Sie mit ihm einen Hash für die Zeichenkette »123456« erzeugen (sie ist laut einer Studie [Anm.: http://tinyurl.com/lesuj7p] der Universität Cambridge das am häufigsten verwendete Passwort überhaupt), erhalten Sie den folgenden Wert:

e150a1ec81e8e93e1eae2c3a77e66ec6dbd6a3b460f89c1d08aecf422ee401a0

Befindet sich dieser Hashwert nun in einer Rainbow Table für SHA256, kann der Angreifer das dazugehörige Passwort in wenigen Minuten ermitteln.

Hat der Programmierer beim Speichern des Passwortes hingegen einen Salt-Wert verwendet – das heißt, hat er die ursprüngliche Zeichenkette »123456« noch um einen zufälligen Wert, z. B. »LeckerSalz« – ergänzt, dann sieht der Hashwert wie folgt aus:

0e15d471b5bb5d70fa9c8eca6777a4c7ecf97becb72ee0dee6cee56b617a40d6

Es müsste jetzt mit dem Teufel zugehen, dass der Angreifer eine Rainbow Table besitzt, die einen Hashwert für »LeckerSalz123456« enthält. Denn die Länge des ursprünglichen Passwortes ist durch das Hinzufügen des Salt-Werts von vorher 6 auf jetzt 16 Zeichen angewachsen. Und Rainbow Tables für Passwörter mit 16 Zeichen Länge und einem Zeichenvorrat aus Buchstaben und Zahlen nähmen so viel Speicherplatz in Anspruch, dass sie bisher noch gar nicht erzeugt werden können.

Auf www.freerainbowtables.com sehen Sie beispielsweise, dass die Rainbow Tables für MD5-Hashes (MD5 ist ein weiterer verbreiteter Hash-Algorithmus) für Passwörter von 14 Zeichen Länge und einen Zeichenvorrat von 10 Zeichen (Ziffern von 0–9) bereits 91 Gigabyte belegen. Die Tabellen, die über den gesamten Zeichenvorrat gehen (also Buchstaben, Sonderzeichen und Ziffern enthalten), belegen für Passwortlängen von 8 Zeichen bereits über 1 Terabyte. Die Erzeugung von Rainbow Tables stößt ab einer bestimmten Passwortkomplexität und -länge schlichtweg an physikalische Grenzen. Der Aufwand zum Errechnen der Rainbow Tables ist dann zu hoch, was sich in zu langer Rechenzeit manifestiert, und der Speicherplatz wird so groß, dass er nicht mehr sinnvoll verwaltet werden kann. Was die Rechenzeit angeht: An den jetzt auf www.freerainbowtables.com bereits verfügbaren Rainbow Tables haben Tausende Computer seit 2005 gerechnet. Das Projekt basiert auf dem BOINC-Projekt, dem Berkeley Open Infrastructure for Network Computing, einem Projekt für verteiltes Rechnen im Internet. Zum Zeitpunkt der Manuskripterstellung waren 3.994 Maschinen mit dem Berechnen neuer Rainbow Tables beschäftigt.

Sie sehen also, dass Länge und Komplexität eines Passwortes und die sichere Speicherung mit einem Hash-Algorithmus samt Salz essenziell für den Schutz vor Angreifern sind. Überdies stellt die Verwendung des Salt-Wertes einen weiteren Schutz dar: Der SHA256-Hashwert für das Passwort »123456« ist auf allen Systemen, die kein Salt verwenden, gleich. Ein Angreifer, der die Passwort-Hashes eines Systems entschlüsseln konnte, kann daher, wenn er dieselben Hashes auf einem anderen System findet, diese mit den bereits gefundenen vergleichen und kennt bei identischen Hashes umgehend das Passwort. Die Verwendung von Salt-Werten umgeht diese Gefahr. Verwendet jedes System einen eigenen Salt-Wert, sind die Passwort-Hashes verschiedener Systeme nicht mehr vergleichbar, auch wenn es sich um dieselben Passwörter handelt.

Nach diesem trockenen Ausflug in die Theorie des Passwort-Knackens sollte jetzt Zeile 2 der Methode generateSHA256 verständlich sein.

In Zeile 3 wird ein NSMutableString mit der Länge des Hashwertes erzeugt (64 Zeichen). Die Konstante CC_SHA256_DIGEST_LENGTH ist in CommonCrypto definiert und gibt die Länge des Hashwertes eines SHA256-Hashs zurück. Der NSMutableString nimmt später den Hashwert des Passworts auf.

CommonCrypto ist keine Cocoa-Touch-Bibliothek, sondern in C geschrieben. Daher erwartet sie als Parameter C-Datentypen. In Zeile 4 wird daher ein char-Array definiert, das die von CommonCrypto erzeugte Zeichenkette, also den Hashwert des Passworts, aufnimmt.

unsigned char passwordChars[CC_SHA256_DIGEST_LENGTH];

Genau wie der NSMutableString aus Zeile 3 bekommt auch dieses Array die Länge des Hashwerts als Kapazität zugewiesen.

Zeile 5 ist dann der eigentliche Aufruf der Hashfunktion:

CC_SHA256([passwordWithSalt UTF8String],  
[passwordWithSalt
lengthOfBytesUsingEncoding:NSUTF8StringEncoding],
passwordChars);

Die Funktion aus CommonDigest ist CC_SHA256 und übernimmt als ersten Parameter den Klartext als C-Zeichenkette, als zweiten die Länge des Klartextes und als dritten Parameter das C-Array, in dem der Hashwert gespeichert wird. Die Umwandlung des NSMutableStrings in eine C-Zeichenkette erfolgt mit der NSString-Methode UTF8String, die eine nullterminierte, in UTF-8 kodierte C-Zeichenkette zurückliefert. Die für den zweiten Parameter verwendete Methode lengthOfBytesUsingEncoding gibt die Länge der Zeichenkette in der betreffenden Kodierung zurück.

Die abschließende for-Schleife iteriert durch die Zeichen des in der C-Zeichenkette abgelegten Hashwerts und schreibt diese in den NSMutableString, der in Zeile 3 erzeugt wurde. Der Formatstring-Parameter sorgt für die byteweise Ausgabe als Hexadezimalwert. Die Methode terminiert abschließend mit der Rückgabe des Hashwerts.

Damit ist erklärt, was mit dem vom Benutzer eingegebenen Passwort passiert. Es wird zunächst mit einem Salt-Wert versehen und anschließend in einen Hashwert umgewandelt. Das Salt ist ein Wert, den Sie geheim halten können, aber nicht geheim halten müssen. Es nutzt einem Angreifer ohnehin nichts, diesen Wert zu kennen. Im Sinne der Zufälligkeit ist es allerdings ratsam, für jedes System ein anderes Salt zu verwenden. Wichtig ist nur, dass Sie beim Speichern des Passwortes denselben Salt-Wert verwenden wie beim Überprüfen.

Sie können statt der hart in den Quellcode verdrahteten Zeichenkette (die im Übrigen ein Verstoß gegen Top 7 aus Abschnitt 7.2.1, »Arten von Sicherheitslücken«, ist) auch einen Wert nehmen, der pro iDevice eindeutig ist. (Anmerkung: Der Verstoß gegen Top 7 ist nicht wirklich ein Verstoß, denn das Salt ist kein Geheimnis. Unschön ist das Hartkodieren im Quelltext aber allemal.)

Ein möglicher Wert wäre die UDID des iDevice, ab iOS 7 der identifierForVendor. Um diesen zu verwenden, fügen Sie vor Zeile 2 in der Methode generateSHA256 noch eine Zeile ein und ändern die bisherige Zeile 3 wie folgt:

...
2. UIDevice *thisDevice = [UIDevice currentDevice];
3. NSString *passwordWithSalt = [NSString
stringWithFormat:@"%@%@", [[thisDevice
identifierForVendor] UUIDString], inputString];
...

Listing 7.15 Dynamisches Salt für iOS 6

Die Konstante SALT können Sie anschließend aus der Headerdatei von SecUtils löschen.

Sie wissen jetzt, wie Sie mit Passwörtern umgehen. Allerdings müssen Sie das Passwort des App-Benutzers jetzt noch persistent abspeichern. Dazu ist die Keychain von iOS der vorgesehene und richtige Ort.

Verwaltung von Benutzerdaten

Die Authentisierung, die wir soeben in das Fototagebuch implementiert haben, zeigt die sicherheitstechnischen Minimalanforderungen an einen Authentisierungsmechanismus. In ihr sind keine Funktionen enthalten, die dem Benutzer die Verwaltung seiner Zugangsdaten erlauben. Grundlegende Verwaltungstätigkeiten sind das Ändern des Benutzerpasswortes oder die Möglichkeit, trotz eines vergessenen Passwortes auf die Daten zuzugreifen. Mit den in diesem Kapitel vorgestellten Techniken sind Sie aber in der Lage, solche Verwaltungsfunktionen selbst zu implementieren.


Rheinwerk Computing - Zum Seitenanfang

7.4.2KeychainZur nächsten ÜberschriftZur vorigen Überschrift

Die Keychain ist unter iOS, genauso wie bei OS X, der zentrale Ort zur Ablage von Geheimnissen. Die Sicherheit der Keychain resultiert aus einem mehrstufigen Verschlüsselungskonzept. Die Keychain ist eine Datenbank, in der jede Tabelle eine verschlüsselte Spalte besitzt, in der die verschlüsselt abzulegenden Daten (die Geheimnisse) liegen. Diese Daten sind mit einem gerätespezifischen Schlüssel versehen, der den transparenten Zugriff auf die Daten auf das jeweilige Gerät beschränkt.

Ein Angreifer, der sich der Datenbankdaten der Keychain bemächtigt, kann die Verschlüsselung außerhalb des Gerätes nicht brechen. Die Keychain ist also grundsätzlich davor geschützt, von einem Gerät extrahiert und per Brute-Force-Angriff (beispielsweise mit einem GPU-Cluster oder einer leistungsfähigen Cloud) dechiffriert zu werden. Trotzdem besteht die Gefahr, dass ein Angreifer auf einem Gerät mit Jailbreak oder über eine Sicherheitslücke im iOS Zugriff auf die Keychain bekommt und unbefugt Daten ausliest.

Seit iOS 4 ist die Keychain zusätzlich zum Geräteschlüssel mit dem Benutzercode des Anwenders verschlüsselt. Das heißt, der Zugriff auf die verschlüsselten Daten ist nur möglich, wenn der Anwender den Benutzercode beim Entsperren des iPhones eingegeben hat. Das schützt neben dem oben genannten Angriff durch Extraktion der Keychain vom Gerät auch gegen die Angriffsvariante, bei der sich ein Unbefugter auf dem Gerät selbst Zugriff auf die Keychain verschafft, um Daten auszulesen.

Die Bedeutung des Benutzercodes in iOS

Der Benutzercode, mit dem ein Anwender sein iPhone vor unbefugtem Zugriff schützen kann und der dem Passwort von Desktop-PCs entspricht, hat dem Konzept der Keychain zufolge nicht nur die Aufgabe, das Gerät vor unbefugtem Zugriff zu schützen, sondern wird auch bei der Verschlüsselung sensibler Daten verwendet.

Daher ist es dringend angeraten, immer einen Benutzercode zu verwenden. Je nach Schutzbedarf ist die Verwendung des standardmäßig auf vier Zahlen eingestellten Benutzercodes nicht ausreichend. Dann sollte ein komplexerer und längerer Benutzercode zum Einsatz kommen. Wenn Sie als App-Entwickler im Auftrag eine App für die Verarbeitung sensibler Daten erstellen, sollten Sie Ihren Auftraggeber auf diesen Umstand hinweisen. Gerade im Unternehmenseinsatz ist die Verwendung starker Passwörter üblich.

Der lokale Angriff auf den Benutzercode über das Booten eines modifizierten Jailbreaks dauert bei einem vierstelligen, aus Zahlen bestehenden Benutzercode auf einem iPhone 4 knapp 30 Minuten – ein nahezu wirkungsloser Schutz.

Wenn Sie sich im Consumer-Umfeld nicht darauf verlassen wollen, dass ein Benutzer sein Gerät mit einem Benutzercode sperrt, müssen Sie eine eigene Verschlüsselung verwenden. Das Fototagebuch speichert genau aus diesem Grund das Benutzerpasswort nicht im Klartext in der Keychain, auch wenn es dort verschlüsselt liegt. Beim Einsatz auf einem Gerät ohne Benutzercode könnte ansonsten ein Angreifer, der Zugriff auf die Keychain erhält, das Passwort auslesen. Die Verwendung einer starken Hashfunktion mit Salt stellt daher eine zweite Sicherungsschicht dar (Defense in Depth), so dass die Sicherheit des Fototagebuch-Passwortes nicht allein davon abhängt, ob der Benutzer einen Benutzercode vergeben hat oder nicht.

Die Keychain kennt vier Arten von Informationen:

  • Generic Passwords
  • Internet Passwords
  • Certificates
  • Keys

Generic Passwords ist die Klasse für App-Programmierer, die Kombinationen aus Benutzername und Passwort sicher ablegen möchten (Key-Value-Paare). Internet Passwords ist der Speicher für Systemdienste von iOS, z. B. für das Mail-Programm oder Safari. Dort liegen die Zugangsdaten zu Mail-Konten, Exchange-Server, Anmeldedaten für Webseiten etc. In der Klasse Certificates speichert iOS alle Zertifikate ab, die der Benutzer auf das Gerät bringt, also z. B. S/MIME oder VPN-Zertifikate. Keys ist ein Systembereich, mit dem Sie als App-Programmierer keine Berührung haben.

Jede App darf nur auf die Keychain-Einträge zugreifen, die sie selbst angelegt hat. Dieser Zugriff wird von iOS über Entitlements geregelt. Sie als Programmierer müssen sich darum also nicht kümmern. Ein wichtiger Punkt, um den Sie sich allerdings kümmern müssen, ist der Schutz der einzelnen Einträge Ihrer App in der Keychain. Dies ist sogar der wichtigste Punkt in Bezug auf die Sicherheit der Keychain, so dass Sie diesen Mechanismus gut kennen sollten, um die Daten Ihrer App bestmöglich gegen unbefugten Zugriff zu schützen. Beim Anlegen eines Eintrages können und sollten Sie eine der folgenden Schutzklassen (Protection Classes) verwenden, die den Zugriff auf den Eintrag regeln:

  • kSecAttrAccessibleAfterFirstUnlock
  • kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
  • kSecAttrAccessibleAlways
  • kSecAttrAccessibleAlwaysThisDeviceOnly
  • kSecAttrAccessibleWhenUnlocked
  • kSecAttrAccessibleWhenUnlockedThisDeviceOnly

Die Konstanten haben die folgende Bedeutung:

  • kSecAttrAccessibleAfterFirstUnlock
    Der Eintrag ist nach dem Booten von iOS erst dann zugänglich, wenn der Benutzer seinen Benutzercode korrekt eingegeben hat. Wird das Gerät gesperrt, bleibt der Eintrag zugänglich. Nach einem Neustart des Geräts muss der Benutzer erst wieder den korrekten Benutzercode eingeben, damit iOS den Eintrag entschlüsselt.
  • kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
    Dieses Attribut entspricht dem vorherigen, allerdings mit dem Unterschied, dass der damit geschützte Eintrag ausschließlich auf dem Gerät verbleibt, auf dem er angelegt worden ist. Installiert der Benutzer ein Geräte-Backup auf ein neues Gerät, wandert der Eintrag nicht mit.
  • kSecAttrAccessibleAlways
    Der Eintrag ist immer zugänglich. Bei der Migration auf ein neues Gerät wandert der Eintrag mit, wenn die Migration über ein in iTunes verschlüsseltes Backup läuft. Dies ist das Attribut mit der schlechtesten Schutzfunktion, und Sie sollten es entweder gar nicht oder nur nach sehr sorgfältiger Abwägung verwenden. Leider bekleckert Apple sich an dieser Stelle nicht mit Ruhm und speichert durchaus sensible Daten wie WLAN-Schlüssel, Zugangsdaten für Exchange- und Mail-Konten sowie Passwörter von Webseiten mit diesem Attribut ab. Nehmen Sie sich dieses Verhalten nicht zum Vorbild!
  • kSecAttrAccessibleAlwaysThisDeviceOnly
    Dieses Attribut entspricht dem vorherigen, beschränkt den Eintrag aber auf das originäre Gerät. Der Eintrag wandert bei einem Backup nicht mit auf das neue Gerät, auch nicht beim Einsatz eines verschlüsselten Backups.
  • kSecAttrAccessibleWhenUnlocked
    Dieses Attribut legt fest, dass ein Eintrag nur dann zugänglich ist, wenn das Gerät entsperrt ist. Jedes Sperren des Gerätes verschlüsselt den Eintrag, unabhängig davon, ob das Gerät nach einem Reboot, vom Benutzer oder durch die automatische Bildschirmsperre gesperrt wurde.
  • kSecAttrAccessibleWhenUnlockedThisDeviceOnly
    Dieses Attribut entspricht dem vorherigen, beschränkt den Eintrag aber auch auf das Gerät, auf dem der Eintrag erzeugt wurde.

Bei der Verwendung der Schutzklassen sollten Sie so restriktiv wie möglich und so freigiebig wie nötig vorgehen. Behalten Sie dabei auch stets den Benutzerkomfort im Auge, denn zu stark übertriebene Sicherheit bringt häufig keinen praktischen Mehrwert, sondern verringert die Produktivität.

Im Beispiel des Fototagebuchs muss das Benutzerpasswort in der Keychain nur dann entschlüsselt vorliegen, wenn das Gerät entsperrt ist – ein Benutzer kann das Fototagebuch ja nur dann öffnen, wenn er das Gerät benutzt. Die Schutzklasse kSecAttrAccessibleWhenUnlocked ist also eine gute Wahl. Die Schutzklasse kSecAttrAccessibleWhenUnlockedThisDeviceOnly wäre auch denkbar. Sie erhöht die Sicherheit allerdings nur unwesentlich, kann aber ein mächtiger Spaßkiller werden. Wenn ein Benutzer über ein Backup die Daten des Fototagebuchs auf ein neues Gerät spielt, wird das in der Keychain gespeicherte Passwort nicht mitwandern. Der Eintrag, dass ein Passwort gesetzt ist, wandert aber sehr wohl mit. Was wird beim ersten Start auf dem neuen Gerät passieren? Der Benutzer wird sich nicht anmelden können, sofern Sie keine Verwaltungsfunktionen für die Benutzerdaten implementiert haben, mit denen der Benutzer ein neues Passwort vergeben kann. Benutzerfreundlichkeit sieht anders aus.

Einen Sicherheitsgewinn stellt diese Schutzklasse im konkreten Fall aber auch nicht dar, denn da das Passwort selbst hinreichend sicher in der Keychain abgelegt ist (starker Hash-Algorithmus mit Salt), spricht nichts dagegen, diesen Eintrag in ein verschlüsseltes Backup und von dort auf ein neues Gerät wandern zu lassen. Denken Sie also immer daran, dass Sicherheit nicht absolut ist und dass übertriebene Sicherheit mit Sicherheit eines bewirken wird: Ihre Benutzer zu verärgern. Und sichere Software, die niemand benutzt, hilft niemandem.

Die Methode zum Speichern des Benutzerpasswortes bzw. seines Hashwertes ist addKeychainEntry:

01 +(BOOL)addKeychainEntry:(NSString *)entry{
02 NSMutableDictionary *searchDict =
[NSMutableDictionary dictionary];
03 [searchDict setObject:(__bridge id)kSecClassGenericPassword forKey:
(__bridge id)kSecClass];
04 [searchDict setObject:KEYCHAIN_SERVICE forKey:
(__bridge id)kSecAttrService];
05 [searchDict setObject:KEYCHAIN_LABEL forKey:
(__bridge id)kSecAttrLabel];
06 [searchDict setObject:KEYCHAIN_ACCOUNT forKey:
(__bridge id)kSecAttrAccount];
07 NSMutableDictionary *writeDict =
[NSMutableDictionary dictionary];
08 [writeDict setDictionary:searchDict];
09 [writeDict setObject:
(__bridge id)kSecAttrAccessibleWhenUnlocked forKey:
(__bridge id)kSecAttrAccessible];
10 [writeDict setObject:[entry dataUsingEncoding:NSUTF8StringEncoding] forKey:
(__bridge id)kSecValueData];
11 NSMutableDictionary *updateDict =
[NSMutableDictionary dictionary];
12 [updateDict setObject:
[entry dataUsingEncoding:NSUTF8StringEncoding]
forKey:(__bridge id)kSecValueData];
13 if(SecItemAdd((__bridge CFDictionaryRef)
writeDict, NULL) == errSecDuplicateItem){
14 NSLog(@"[+] Old password found – updating");
15 SecItemUpdate((__bridge CFDictionaryRef)
searchDict, ((__bridge CFDictionaryRef)updateDict));
16 }
17 return YES;
18 }

Listing 7.16 Die Methode »addKeychainEntry«

Der schreibende Zugriff auf die Keychain erfolgt in dieser Methode über die beiden Methodenaufrufe SecItemAdd und SecItemUpdate. Der erste fügt, wie der Name vermuten lässt, einen neuen Eintrag in die Keychain ein, und der zweite modifiziert einen bestehenden Eintrag. Dass im vorliegenden Beispiel beide Methoden verwendet werden, ist dem Umstand geschuldet, dass vorherige Installationen des Fototagebuchs unter Umständen schon ein Passwort in der Keychain abgelegt haben. Versucht eine neue Installation der App jetzt ein Passwort mit denselben Attributen hinzuzufügen, wird es einen Fehler geben. In diesem Fall kommt SecItemUpdate zum Zug, mit dem dann der bereits bestehende Eintrag modifiziert wird.

App weg, Keychain-Eintrag nicht

Beim Löschen einer App vom iDevice werden alle Daten entfernt, die in der Sandbox dieser App liegen. Nicht entfernt werden hingegen die Keychain-Einträge einer App. Dies müssen Sie als App-Programmierer berücksichtigen – als paranoider Zeitgenosse allerdings auch, denn im Laufe der Zeit wird die Keychain ein wahres Füllhorn voller alter Geheimnisse werden.

Die SecItem*-Methoden erwarten als Parameter Dictionarys, also Schlüssel-Wert-Paare. In der Methode zum Hinzufügen des neuen Passworts zur Keychain werden drei Dictionarys benötigt. Das erste dient zum Auffinden eines eventuell bereits vorhandenen Eintrags (searchDict, Zeile 2). Das zweite, writeDict, das zum Anlegen eines neuen Passwort-Eintrags dient, enthält alle Werte und Schlüssel von searchDict, fügt darüber hinaus die Schutzklasse und den eigentlichen Passwort-Hash hinzu. Das dritte Dictionary, updateDict, enthält lediglich den Passwort-Hash und dient zum Erneuern eines bereits vorhandenen Eintrags.

Tabelle 7.9 zeigt eine Übersicht über alle Werte, die ein Passwort-Eintrag vom Fototagebuch in der Keychain ablegt.

Tabelle 7.9 Passwort-Eintrag des Fototagebuches in der Keychain

Key Object

kSecClass

kSecClassGenericPassword

kSecAttrService

KEYCHAIN_SERVICE (aus SecUtils.h)

kSecAttrLabel

KEYCHAIN_LABEL (aus SecUtils.h)

kSecAttrAccount

KEYCHAIN_ACCOUNT (aus SecUtils.h)

kSecAttrAccessible

kSecAttrAccessibleWhenUnlocked

kSecValueData

Passwort-Hash

Die SecItem*-Methoden sind Methoden aus dem Core Foundation Framework. Die als Parameter erwarteten Dictionarys vom Typ CFDictionary sind daher Datentypen aus dem Core Foundation Framework. Beim Übergang zum Core Foundation Framework muss der Programmierer bei der Verwendung von ARC bestimmen, welche Speicherregeln die Datentypen des Frameworks verwenden sollen.

Die hier verwendete Speicherregel ist __bridge, was bedeutet, dass zwar ein Zeiger übergeben wird, der Besitzer des Zeigers aber unverändert bleibt. Es erfolgt also lediglich die Anweisung, dass sich das Dictionary ARC-konform verhalten soll, sich darüber hinaus aber um nichts weiter kümmern muss.

SecItemAdd erwartet als Parameter das Dictionary mit den passenden Wert-Schlüssel-Paaren. Da die drei Dictionarys der Methode addKeychainEntry als NSMutableArray in Objective-C implementiert sind, müssen sie vor dem Übergang an das Core Foundation Framework in den Typ CFDictionary umgewandelt werden. Diese explizite Typumwandlung erfolgt als Parameter zum __bridge-Befehl:

((__bridge CFDictionaryRef)

Der zweite Parameter von SecItemAdd ist eine Referenz auf das neu in die Keychain eingefügte Objekt. Im vorliegenden Fall wird eine solche Referenz für den weiteren Programmverlauf nicht benötigt, daher ist der Parameter NULL.

Ist der von SecItemAdd anzulegende Keychain-Eintrag bereits vorhanden, liefert die Methode den Rückgabewert –25299 (errSecDuplicateItem). Die Prüfung auf den Rückgabewert in Zeile 13 verzweigt daher auf den alternativen Aufruf von SecItemUpdate in Zeile 15.

SecItemUpdate erwartet als Parameter zwei Dictionarys. Das erste ist das Dictionary, das benutzt wird (hier: searchDict), um den Eintrag in der Keychain zu suchen, der geändert werden soll. Das zweite enthält die zu ändernden Daten (hier: updateDict).

Die letzte Methode von SecUtils ist die zum Auslesen des Passwort-Hashs aus der Keychain:

01 +(NSString *)getUserPwFromKeychain{
02 NSMutableDictionary *query = [NSMutableDictionary dictionary];
03 [query setObject:(__bridge NSString
*)kSecClassGenericPassword forKey:
(__bridge NSString *)kSecClass];
04 [query setObject:KEYCHAIN_ACCOUNT forKey:
(__bridge id)kSecAttrAccount];
05 [query setObject:KEYCHAIN_LABEL forKey:
(__bridge id)kSecAttrService];
06 [query setObject:KEYCHAIN_SERVICE forKey:
(__bridge id)kSecAttrService];
07 [query setObject:(id)kCFBooleanTrue
forKey:(__bridge_transfer id)kSecReturnData];
08 CFDataRef pw = nil;
09 OSStatus status = SecItemCopyMatching
((__bridge CFDictionaryRef)query, (CFTypeRef*)&pw);
10 if(status != noErr){
11 NSLog(@"[+] Error reading PW from Keychain");
12 }
13 NSData *result = (__bridge_transfer NSData*)pw;
14 NSString *storedPassword = [[NSString
alloc] initWithData:result encoding:NSUTF8StringEncoding];
15 return storedPassword;
16 }

Listing 7.17 Die Methode »getUserPwFromKeychain«

Die Verwendung von SecItemCopyMatching ist analog zu SecItemAdd oder SecItemUpdate. Sie übergeben als ersten Parameter ein Dictionary mit den Suchkriterien, und der zweite Parameter ist ein Zeiger auf einen Datenpuffer vom Typ CFDataRef (definiert in Zeile 8).

Der Aufruf von SecItemCopyMatching in Zeile 9 enthält dieselben __bridge-Anweisungen wie bei den anderen SecItem*-Methoden. Neu ist die Verwendung des Adressoperators bei der Übergabe des zweiten Parameters. Das & vor dem Namen der Puffervariablen weist SecItemCopyMatching an, das Ergebnis der Keychain-Abfrage an die Speicheradresse von pw zu schreiben – tiefste C-Abgründe.

Da CFDataRef zum Core Foundation Framework gehört, ist an dieser Stelle keine Verwendung von __bridge notwendig. __bridge kommt erst da zum Einsatz, wo die Daten vom CF-Datentyp in Objective-C umgewandelt werden, nämlich bei der Zuweisung in ein NSData in Zeile 13.

Damit sollten Sie jetzt alles beisammen haben, um das Fototagebuch mit einer Authentisierungsfunktion zu versehen. Implementieren Sie alle Klassen und Methoden wie oben beschrieben, und denken Sie an die Importanweisungen in allen Headerdateien.

Bevor Sie das Projekt übersetzen und ausführen können, müssen Sie allerdings noch das Security Framework zum Projekt hinzufügen, da darin die Keychain-Zugriffe gekapselt sind. Öffnen Sie die Projekt-Zusammenfassung, und fügen Sie zu Linked Frameworks and Libraries das Security.framework hinzu.

Abbildung

Abbildung 7.32 Hinzufügen des Security Frameworks

Wenn Sie alles richtig gemacht haben, das Projekt ausführen und beim ersten Start des Fototagebuchs ein neues Passwort vergeben haben, werden Sie – sofern Sie die NSLog-Anweisungen in der Methode loginUser des LoginViewController nicht auskommentiert haben – folgende Einträge in der Konsolenausgabe sehen (die Ausgabe auf Ihrem Rechner wird sich natürlich hiervon unterscheiden, weil sie vom verwendeten Passwort abhängt):

[...] PhotoDiary[44559:c07] [+] Password hash: 995751b2ae2825ac282887faee1f135914a37d166e178bdaf3dbbb9a97715159
[...] PhotoDiary[44559:c07] [+] Password: 1234

Listing 7.18 Konsolenausgabe von Passwort und Hash

Jede der beiden NSLog-Ausgaben stellt eine Bedrohung aus der STRIDE-Kategorie Repudiation dar! Ein Angreifer, der Zugriff auf die Log-Dateien eines iDevice hat, kann daraus die überaus sensiblen Daten Passwort und Hash dieses Gerätes auslesen. Der Angreifer kann die Log-Daten problemlos über den Xcode-Organizer einsehen. Und es ist auch denkbar, dass sich ein Angreifer über einen Jailbreak Zugriff auf das iDevice verschafft.

Daher dürfen Sie niemals solche Informationen in Log-Dateien schreiben! Dies war ein mit Bedacht gewähltes, aber leider typisches Beispiel dafür, dass man als Entwickler den üblichen Weg des Loggings geht, um beim Entwickeln möglichst viele Informationen zu haben, darüber aber aus den Augen verliert, dass man damit eine große Sicherheitslücke aufreißt.


Rheinwerk Computing - Zum Seitenanfang

7.4.3Jailbreak-ErkennungZur nächsten ÜberschriftZur vorigen Überschrift

Sie haben in diesem Kapitel bereits mehrfach den Begriff Jailbreak gelesen. Unter einem Jailbreak versteht man das Modifizieren des Betriebssystem-Kernels, so dass dieser das Starten unsignierten Codes erlaubt und das Sandboxing deaktiviert. Tools zum Durchführen von Jailbreaks sind im Internet frei verfügbar und obendrein leicht zu bedienen, so dass auch Otto Normaluser sein iDevice mit einem Jailbreak versehen kann.

Beim Umgang mit iOS muss man ein Gerät mit Jailbreak grundsätzlich als unsicher betrachten. Nicht nur, dass ein Jailbreak die wichtigsten Sicherheitsmechanismen von iOS außer Kraft setzt, überdies erfordert die Installation eines Jailbreaks die Verwendung von Tools und Firmware, die aus unbekannten Quellen stammen. Die Natur des Jailbreaks bringt es mit sich, dass er unter Umständen von einem Angreifer über eine Sicherheitslücke auf ein iPhone appliziert wird, ohne dass der Anwender etwas davon bemerkt. Entsprechende Beispiele sind in der Vergangenheit bereits durch die Presse gegangen. [Anm.: http://www.jailbreakme.com]

Sie sollten daher als App-Programmierer Vorsorge treffen, um Geräte mit Jailbreak zu erkennen, und gegebenenfalls – je nach Sensibilität der von der App verarbeiteten Daten – geeignete Maßnahmen ergreifen, um auf einen Jailbreak zu reagieren. Die Verwendung einer App für Online-Banking ist auf einem Gerät mit Jailbreak ebenso wenig zu empfehlen wie Online-Banking auf einem PC, der mit einem Trojaner befallen ist. Besondere Beachtung sollten diesem Umstand diejenigen Programmierer schenken, die Apps für firmeninterne Anwendungen schreiben, sogenannte Enterprise Apps, mit denen Firmen ihre Mitarbeiter ausstatten und mit denen Mitarbeiter auf zentrale Systeme wie z. B. CRM- oder HR-Systeme zugreifen.

Jailbreak-Erkennung und Cracking von Apps

Als App-Programmierer müssen Sie zwei Themen auseinanderhalten: Jailbreak und Cracking. Die primäre Motivation für einen Jailbreak ist, die von Apple im Betriebssystem implementierten Restriktionen und Sicherungsmechanismen zu umgehen. Darüber hinaus besteht auf einem Gerät mit Jailbreak die Möglichkeit, Apps zu cracken, d. h. den von Apple implementierten Kopierschutz zu entfernen und eine App zu verwenden, ohne sie vorher im App Store gekauft zu haben.

Verfallen Sie als App-Programmierer, der eine kostenpflichtige App im Apple App Store hat, nicht dem Trugschluss, dass jeder Benutzer mit Jailbreak Ihre (und andere) Apps verwenden will, ohne dafür zu bezahlen. Natürlich gibt es immer schwarze Schafe, aber viele Benutzer installieren sich Jailbreaks lediglich, um die Funktionen ihres iDevice zu erweitern. Wenn Sie diese Benutzer dadurch benachteiligen, dass Ihre App auf ihren Geräten nicht mehr läuft, weil Sie über eine Jailbreak-Erkennung die App automatisiert deaktivieren, werden Sie nur provozieren, dass Ihre App erst recht gecrackt wird.

Trennen Sie daher Jailbreak-Erkennung und Lizenzierung voneinander. Ein Jailbreak ist ein Sicherheits- und kein Lizenzproblem. Die Lizenzierung können Sie z. B. über Onlineanfragen an Ihren Server prüfen und nicht lizenzierte Apps auf diese Weise ausschließen. Dagegen hilft dann auch kein Cracken der App.

Alle bisher bekannten Jailbreaks zeichnen sich durch verschiedene, eindeutige Änderungen am Betriebssystem aus, die Sie in Ihrer App abfragen können. Nicht alle Jailbreaks führen alle diese Veränderungen am System durch. Sie dürfen sich also nicht darauf verlassen, eine Änderung zu prüfen, und beim Nichtvorliegen auf die Abwesenheit eines Jailbreaks schließen.

Die grundlegendste Eigenschaft eines Jailbreaks ist das Deaktivieren der Signaturprüfung und des Sandboxings im Kernel. Ein möglicher Test auf einen Jailbreak wäre daher das Ausführen unsignierten Codes aus der eigenen App heraus sowie das Prüfen, ob die Sandbox ordnungsgemäß funktioniert.

Beim Wald-Feld-und-Wiesen-Jailbreak, den Benutzer installieren, um den Funktionsumfang ihres iDevice zu erweitern, sind immer bestimmte Verzeichnisse und Dateien vorhanden, die den Jailbreak als solchen identifizieren. Das Prüfen auf das Vorhandensein dieser Dateien ist daher ebenfalls Bestandteil einer Jailbreak-Erkennung.

Die in Listing 7.19 dargestellte Methode zur Prüfung auf das Vorhandensein eines Jailbreaks enthält die folgenden Überprüfungen des Systems:

  • Überprüfung, ob es sich um ein echtes iDevice oder einen Simulator handelt
  • Vorhandensein der Cydia-App
  • Vorhandensein des OpenSSH-Servers
  • Vorhandensein des apt-Paketmanagers
  • Vorhandensein der Bash
  • Vorhandensein des MobileSubstrate-Frameworks
  • Versuch, einen Prozess zu forken
  • Überprüfung, ob /Applications ein symbolischer Link ist

Andere Versionen, andere Sitten

Beachten Sie bei der Jagd auf Jailbreaks, dass sich das Verhalten von iOS von Version zu Version ändert. Vor iOS 7 war es Apps erlaubt, den Inhalt der Datei /etc/fstab zu lesen und damit die Mount-Optionen der Root-Partition zu ermitteln. Jailbreaks haben die Root-Partition statt readonly stets als writable gemountet. Mit iOS 7 unterbindet die Sandbox das Lesen der Datei /etc/fstab.

Umgekehrt war es in iOS-Versionen vor iOS 7 nicht erlaubt, in das tmp-Verzeichnis zu schreiben (/tmp). Seit iOS 7 ist Apps dies erlaubt. Passen Sie Ihre Jailbreak-Prüfung daher immer sorgfältig an die jeweilige iOS-Version an.

Insbesondere die Überprüfung auf das Vorhandensein bestimmter Dateien und Verzeichnisse steht exemplarisch dafür, dass eine umfassende Überprüfung auf einen Jailbreak noch auf wesentlich mehr Dateien und Verzeichnisse prüfen sollte.

Ein Wermutstropfen dabei ist, dass all diese Schritte – sowohl das Ausführen unsignierten Codes durch eine App als auch das Lesen und Schreiben im Dateisystem außerhalb der eigenen Sandbox – durch Apples Vorschriften für den App Store [Anm.: https://developer.apple.com/appstore/resources/approval/guidelines.html] verboten sind. Damit beschränkt sich das Einsatzgebiet einer Jailbreak-Prüfung auf Apps, die über einen firmeninternen Enterprise App Store verteilt werden.

Die Prüfung auf das Vorhandensein eines Jailbreaks lässt sich bequem in eine Methode packen. Im Folgenden werden Sie die Klasse SecUtils des Fototagebuchs um eine solche Methode erweitern, so dass die App beim Start prüfen kann, ob sie auf einem Gerät mit Jailbreak läuft oder nicht.

Fügen Sie in der Headerdatei von SecUtils (SecUtils.h) die folgende Methodendeklaration hinzu:

+ (BOOL)checkJailbreak;

Die Implementierung der Methode in SecUtils.m sieht so aus:

+ (BOOL)checkJailbreak{
NSLog(@"[+] %@", NSStringFromSelector(_cmd));

NSRange range = [[[UIDevice currentDevice]
localizedModel] rangeOfString:@"Simulator" options:NSCaseInsensitiveSearch];
if(range.location != NSNotFound)
{
NSLog(@"[+] Running on Simulator");
return NO;
}
NSInteger forecast = 0;

// root partition rw
NSError *error = nil;

// locate Cydia
NSString *docsDir = @"/Applications/";
NSFileManager *localFileManager=[[NSFileManager
alloc] init];
error =nil;
NSArray *dirEnum = [localFileManager
contentsOfDirectoryAtPath:docsDir error:&error];

for(id object in dirEnum){
if([object isEqual:@"Cydia.app"]){
forecast += 16;
NSLog(@"[+] Cydia found");
}
}

// find sshd
if([localFileManager fileExistsAtPath:@"/usr/sbin/sshd"]){
forecast += 16;
NSLog(@"[+] sshd found");
}

// find apt
if([localFileManager
fileExistsAtPath:@"/private/var/lib/apt"]){
forecast += 16;
NSLog(@"[+] apt found");
}

// find bash
if([localFileManager fileExistsAtPath:@"/bin/bash"]){
forecast += 16;
NSLog(@"[+] bash found");
}

// find MobileSubstrate
if([localFileManager
fileExistsAtPath:@"/Library/MobileSubstrate/MobileSubstrate.dylib"]){
forecast += 16;
NSLog(@"[+] MobileSubstrate found");
}

// fork()
int res = fork();
if(res>0){
forecast += 16;
NSLog(@"[+] fork successfull");
} else {
NSLog(@"fork: %s\n", strerror(errno));
}

// symlink
NSFileManager *file = [NSFileManager defaultManager];
NSDictionary *fileDic = [file
attributesOfItemAtPath:@"/Applications" error:&error];
NSString *type = [fileDic objectForKey:NSFileType];
if(type != NSFileTypeDirectory){
forecast += 16;
NSLog(@"type: %@", type);
} else {
NSLog(@"type: %@", type);
}

NSLog(@"[+] forecast: %d%%", forecast);

if(forecast >= 32) // adjust for probability
return YES;
else
return NO;
}

Listing 7.19 Überprüfung auf einen Jailbreak

Importieren Sie nach der Erweiterung der Klasse SecUtils noch SecUtils.h im AppDelegate (PhotoDiaryAppDelegate.h), und rufen Sie in der Methode inApplication didFinishLaunchingWithOptions im AppDelegate die Methode checkJailbreak von SecUtils auf:

- (BOOL)application:(UIApplication *)  
inApplication didFinishLaunchingWithOptions:
(NSDictionary *)inLaunchOptions {
[SecUtils checkJailbreak];
[...]

Listing 7.20 Aufruf von »checkJailbreak« im »AppDelegate«

Aussagekraft der Jailbreak-Prüfung

Aus der PC-Welt weiß man, dass sich Schadsoftware, die auf Kernel-Ebene agiert, beliebig tarnen kann und dass somit alle Versuche, ihr auf die Schliche zu kommen, ins Leere führen. Eine über einen Jailbreak entsprechend modifizierte Firmware könnte dies auch mit den vorstehend beschriebenen Prüfschritten tun. Es ist daher etwas naiv, ein kompromittiertes System zu befragen, ob es kompromittiert ist. Aber das sollte Sie nicht daran hindern, trotzdem auf einen Jailbreak zu prüfen. Im PC-Universum lebt eine ganze Industrie davon, Systeme auf ihren Zustand hin zu untersuchen, und das mit häufig fragwürdigen Ergebnissen. [Anm.: http://heise.de/-1500396]

Bis heute ist aber noch keine Schadsoftware auf Betriebssystemebene für iOS bekannt geworden. Daher liefert die manuelle Prüfung auf einen Jailbreak innerhalb der App zurzeit eine angemessen verlässliche Aussage. Was die Zukunft bringt, bleibt abzuwarten. Sicher ist nur eins: Es wird in nicht allzu ferner Zukunft eine Zeit kommen, in der öffentlich verfügbare Jailbreaks aussterben werden. Apple schließt bekannt gewordene Sicherheitslücken und vergrößert stetig seine Anstrengungen, Software sicher(er) zu machen.


Rheinwerk Computing - Zum Seitenanfang

7.4.4Verzeichnisse und DateiattributeZur nächsten ÜberschriftZur vorigen Überschrift

Der Schutz von Dateien auf dem iPhone erfolgt ähnlich wie der Schutz der Keychain-Einträge. Auch hier ist eine mögliche Verschlüsselung an den Benutzercode des Benutzers gekoppelt. Mit iOS 4 hat Apple mit dem NSFileProtectionKey zwei mögliche Schutzklassen eingeführt:

  • NSFileProtectionNone: Die Datei ist nicht verschlüsselt und immer lesbar.
  • NSFileProtectionComplete: Die Datei ist verschlüsselt und kann nicht gelesen werden, während das Betriebssystem startet oder das Gerät gesperrt ist.

Mit iOS 5 sind zwei weitere Schutzklassen hinzugekommen, die eine feinere Granulierung der Zugriffsrechte erlauben:

  • NSFileProtectionCompleteUnlessOpen: Die Datei ist verschlüsselt und kann nur entschlüsselt werden, während das Gerät entsperrt ist. Danach bleibt die Datei entschlüsselt.
  • NSFileProtectionCompleteUntilFirstUserAuthentication: Die Datei ist verschlüsselt und wird nach dem Start des Betriebssystems erst durch Eingabe des Benutzercodes entschlüsselt. Danach bleibt die Datei unverschlüsselt bis zum nächsten Neustart.

Die Verwendung der Schutzklassen ist unkompliziert. Sie erstellen ein NSDictionary mit Attributen, das Sie dem NSFileManager beim Anlegen der betreffenden Datei als Parameter für den NSFileProtectionKey übergeben:

NSDictionary *attr = [NSDictionary dictionaryWithObject:  

NSFileProtectionCompleteUnlessOpen forKey:
NSFileProtectionKey];
if (![[NSFileManager defaultManager]
setAttributes:attr
ofItemAtPath:fileerror:&error]) {
...
}

Verwendung des »NSFileProtectionKey«

Die sichere Ablage von Dateien über den NSFileProtectionKey ist mit einer zusätzlichen Zeile Code erledigt. Es gibt also keine sinnvolle Ausrede dafür, Dateien nicht zu schützen. Für die Wirksamkeit des Schutzes gilt das, was wir bereits für den Schutz der Keychain gesagt haben: Die Stärke der Verschlüsselung hängt direkt von der Stärke des Benutzercodes ab.

Sie können neben der gezielten Verschlüsselung über NSFileManager auch global festlegen, dass eine App ihre Daten verschlüsseln soll, und zwar in der App ID. Was vor Xcode 5 noch im iOS Provisioning Portal erledigt werden musste, können Sie mit Xcode 5 direkt in Xcode erledigen. Öffnen Sie dazu in den Projekteinstellungen den Tab Capabilities, und aktivieren Sie die Fähigkeit Data Protection.

Abbildung

Abbildung 7.33 Aktivieren Sie die Fähigkeit »Data Protection«.

Xcode fügt dann die Fähigkeit zur App ID hinzu, was einige Minuten dauern kann.

Abbildung

Abbildung 7.34 Warten ...

Nach erfolgreichem Abschluss zeigt Xcode diesen Hinweis an.

Abbildung

Abbildung 7.35 Die App verschlüsselt nun alles.

Der Schutz für Dateien ist nicht nur bei der Verwendung des NSFileManagers verfügbar. Auch SQLite und Core Data können mit dem Verschlüsselungsattribut umgehen. Um das Fototagebuch so zu erweitern, dass die über Core Data erzeugte SQLite-Datenbank mit den Tagebucheinträgen verschlüsselt abgelegt wird, müssen Sie in der Methode storeCoordinator im AppDelegate lediglich zwei Zeilen hinzufügen:

01 – (NSPersistentStoreCoordinator *)storeCoordinator {
02 if(storeCoordinator == nil) {
03 NSURL *theURL = [[self
04 applicationDocumentsURL] URLByAppendingPathComponent:
@"Diary.sqlite"];
05 NSError *theError = nil;
06 NSPersistentStoreCoordinator
07 *theCoordinator = [[NSPersistentStoreCoordinator
08 alloc] initWithManagedObjectModel:self.managedObjectModel];
09 NSDictionary *fileAttributes =
[NSDictionary dictionaryWithObject:
NSFileProtectionComplete forKey:NSFileProtectionKey];
10 [[NSFileManager defaultManager]
setAttributes:fileAttributes ofItemAtPath:
[theURL absoluteString] error:&theError];
11
12 if ([theCoordinator
13 addPersistentStoreWithType:NSSQLiteStoreType
14 configuration:nil
URL:theURL
options:nil
error:&theError]) {
15 self.storeCoordinator = theCoordinator;
16 }
17 else {
18 NSLog(@"storeCoordinator: %@", theError);
19 }
20 }
21 return storeCoordinator;
22 }

Listing 7.21 Die Methode »storeCoordinator« im »AppDelegate«

Neu sind die beiden Zeilen 9 und 10. Zeile 9 definiert ein NSDictionary mit den notwendigen Attributen, und in Zeile 10 werden diese Attribute auf die SQLite-Datenbank von Core Data angewendet. Das war alles.

Bei der Verwendung von NSData können Sie über die Verwendung der NSDataWritingOptions die Verschlüsselung der Daten aktivieren, die über die Schreibmethoden abgelegt wurden. Die Schutzklassen sind dieselben wie beim NSFileManager, nur dass sie mit NSData beginnen:

  • NSDataWritingFileProtectionNone
  • NSDataWritingFileProtectionComplete
  • NSDataWritingFileProtectionCompleteUnlessOpen
  • NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication

Rheinwerk Computing - Zum Seitenanfang

7.4.5Event-HandlingZur nächsten ÜberschriftZur vorigen Überschrift

Ein zentraler Bestandteil der App-Sicherheit ist der richtige Umgang mit Events. Das betrifft zum einen das Abfangen von Fehlermeldungen (Information Disclosure), zum anderen den korrekten Umgang mit Events. Eine App, die aus Sicherheitsgründen eine eigene Authentisierung implementiert, sollte ihr GUI beim Wechsel in den Hintergrund sperren, um unbefugten Zugriff zu verhindern. Auch sollte die App in diesem Fall sensible Daten bereinigen oder verschlüsseln, um zu verhindern, dass diese Daten unbemerkt, z. B. über das automatische iTunes-Backup, das Gerät verlassen.

Die einfachste Form des Event-Handlings besteht darin, das GUI der App beim Auftreten des Events applicationWillResignActive: zu sperren. Korrekterweise handelt es sich dabei nicht um ein Event, sondern um den Aufruf einer Delegate-Methode. Diese Methode wird aufgerufen, wenn die App über das Drücken des Home-Buttons in den Hintergrund geschickt wird oder wenn ein Telefonanruf eingeht. Nimmt der Benutzer den Telefonanruf nicht an, beispielsweise, weil er gerade sehr konzentriert mit Ihrer App arbeitet, kommt die App nach dem Abbruch des Anrufversuchs automatisch wieder in den Vordergrund.

Wenn Sie verhindern wollen, dass der Benutzer sich dann wieder an der App authentisieren muss, sperren Sie das GUI nicht in der Methode applicationWillResignActive:, sondern in der im Anschluss aufgerufenen Methode applicationDidEnterBackground:. Damit stellen Sie sicher, dass die App nur dann gesperrt wird, wenn sie auch wirklich in den Hintergrund geschickt worden ist. Sie haben nach Aufruf von applicationDidEnterBackground: fünf Sekunden Zeit für »Aufräumarbeiten«. Wenn Ihre App die Methode nicht innerhalb von fünf Sekunden abarbeitet, wirft iOS sie gnadenlos aus dem Speicher. Sie können für längere Aufgaben zwar mehr Zeit beim Betriebssystem einfordern, aber das wird weder empfohlen, noch gibt es dafür (in der Regel) einen triftigen Grund.

Abbildung 7.36 und Abbildung 7.37 zeigen den Lebenszyklus einer App auf iOS-Versionen vor 4 (ohne Multitasking) und auf iOS-Versionen ab 4 (mit Multitasking).

Abbildung

Abbildung 7.36 Lebenszyklus einer App vor iOS 4

Abbildung

Abbildung 7.37 Lebenszyklus einer App ab iOS 4


Rheinwerk Computing - Zum Seitenanfang

7.4.6ScreenshotsZur nächsten ÜberschriftZur vorigen Überschrift

iOS ist – wie die meisten aller Apple-Produkte – auch etwas fürs Auge. Man könnte beim iPhone fast sagen: »Das Auge telefoniert ja auch mit.« Ein Schmankerl von iOS ist die hübsche Animation beim Beenden von Apps. Drückt der Benutzer den Home-Button, verkleinert iOS das GUI der gerade aktiven App zum Mittelpunkt des Displays und zeigt das Springboard, den Schreibtisch von iOS.

Dieses Herauszoomen erfolgt auf die billigstmögliche Art: iOS erstellt nach dem Aufruf (genauer gesagt nach Beendigung) der Delegate-Methode applicationDidEnterBackground: einen Screenshot des GUI und verkleinert nur diesen Screenshot. Gegen dieses Vorgehen wäre aus sicherheitstechnischer Sicht nichts einzuwenden, wenn, ja wenn iOS diese Screenshots nicht im Dateisystem ablegen würde.

Da iOS aber genau dies macht, und zwar im Verzeichnis /private/var/mobile/Library/Caches/Snapshots, müssen Sie als App-Programmierer Vorsorge treffen, dass ein solcher vom Betriebssystem angefertigter Screenshot keine sensiblen Daten enthält. Ein Angreifer mit Zugriff auf das Dateisystem (z. B. über eine ins System eingeschleuste Backdoor) kann diese Dateien auslesen und darüber gegebenenfalls sensible Informationen erlangen. In Büchern über forensische Untersuchungen an iOS-Geräten wird stets auf die Snapshots als eine wichtige Datenquelle hingewiesen – zu Recht.

Abbildung

Abbildung 7.38 Sensible Daten im Screenshot

Sie sollten daher auch das GUI Ihrer App aufräumen, nachdem applicationDidEnterBackground: aufgerufen wurde. Analog zum Event-Handling aus Abschnitt 7.4.5 zeigen Sie im einfachsten Fall den Anmeldebildschirm der App, bevor sie in den Hintergrund entschwindet. Damit haben Sie zwei Fliegen mit circa zwei bis einer Klappe geschlagen.


Rheinwerk Computing - Zum Seitenanfang

7.4.7Sorgfältiger Umgang mit der BildschirmsperreZur nächsten ÜberschriftZur vorigen Überschrift

Wie wir bereits in Abschnitt 7.4.2, »Keychain«, beschrieben haben, ist die Verwendung eines Benutzercodes für die sinnvolle Verschlüsselung von Daten unter iOS zwingend notwendig. Nun sind Benutzer gerne vergesslich, so dass man, insbesondere im Unternehmensumfeld, neben dem Benutzercode auch die automatische Sperre des Gerätes aktiviert. Diese führt dazu, dass sich ein entsperrtes iDevice, das über einen definierten Zeitraum hinweg nicht benutzt wird, automatisch sperrt – genauso, wie ein Bildschirmschoner am Arbeitsplatz-Computer erscheint. Dieses Verhalten soll der Bedrohung entgegenwirken, dass ein Unbefugter ein unbeaufsichtigtes Gerät an sich nimmt und darauf zugreifen kann (Spoofing).

Abbildung

Abbildung 7.39 Einstellungen für die automatische Bildschirmsperre

Nun kann es durchaus Apps geben, bei denen die automatische Sperre nicht erwünscht ist. Denken Sie an die Verwendung einer Navigations-App im Auto. Wenn Sie alle fünf Minuten das Gerät mit Ihrem Benutzercode entsperren müssen, weil wieder die automatische Sperre zugeschlagen hat, währt die Freude über die Nagivationshilfe nur kurz.

Praktischerweise besitzt das UIApplication-Objekt die Property idleTimerDisabled, mit der sich das automatische Sperren des Bildschirms verhindern lässt. Sie ahnen vermutlich, warum diese Property im Kapitel über Sicherheit steht? Richtig, die unbedachte Verwendung dieser Property öffnet einen Spoofing-Angriffsvektor gegen das iDevice. Setzen Sie diese Funktion daher nur sparsam ein, d. h. nur dort, wo es sinnvoll ist, und nur so lange, wie es sinnvoll ist.

Sie deaktivieren die Bildschirmsperre am einfachsten direkt im AppDelegate, und dort in der Methode application:didFinishLaunchingWithOptions:

- (BOOL)application:(UIApplication *)  
application didFinishLaunchingWithOptions:
(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
[UIApplication sharedApplication]
.idleTimerDisabled = YES;
return YES;
}

Listing 7.22 Deaktivieren der automatischen Bildschirmsperre

Sofern es geboten ist, beispielsweise bei sicherheitskritischen Apps, sollten Sie den Benutzer über die Deaktivierung der Bildschirmsperre informieren und zu sorgsamem Umgang mit der App raten. Im Unternehmensumfeld sollte eine entsprechende Richtlinie zur Nutzung mobiler Endgeräte (Mobile Device Policy) die Benutzer für den Umgang mit entsprechenden Apps sensibilisieren.


Rheinwerk Computing - Zum Seitenanfang

7.4.8Struktur und Ordnung im SandkastenZur nächsten ÜberschriftZur vorigen Überschrift

iOS ist ein überaus benutzerfreundliches System. Es denkt neben den vielen anderen Dingen, die es im Hintergrund erledigt, auch daran, die Daten auf einem iDevice zu sichern. Das ist vordergründig eine tolle Sache, bei näherem Nachdenken werden Sie aber feststellen, dass dies natürlich Folgen für die Sicherheit haben kann. Denken Sie an unverschlüsselt auf dem iDevice liegende Dateien, an Dateien, die Geheimnisse enthalten und per se das iDevice nicht verlassen sollten, oder an temporäre Dateien.

Seit iOS 5.0.1 gibt es die Möglichkeit, gezielt Dateien und Verzeichnisse vom Backup auszuschließen. Für iOS 5 und ältere Versionen gibt es diese Möglichkeit nicht. Auf diesen älteren Versionen entscheidet der Speicherort einer Datei darüber, ob iOS sie ins Backup aufnimmt oder nicht.

Wie wir in Abschnitt 7.1, »iOS und Hardware«, geschildert haben, besitzt jede App ihr eigenes Dateisystem innerhalb ihrer Sandbox. Das Dateisystem einer App können Sie einsehen, indem Sie das iDevice im Xcode-Organizer öffnen und links in der Navigationsleiste Applications auswählen (dort können Sie übrigens auch Dateien aus der Sandbox auf Ihren Mac übertragen). Abbildung 7.40 zeigt die Sandbox des Fototagebuchs.

Das Verzeichnis Documents ist der vorgesehene Ort für alle Dateien, die der Benutzer oder die App selbst erzeugen. Apple bezeichnet diese Dateien als kritische Daten, die dadurch gekennzeichnet sind, dass sie durch die App oder eine Neuinstallation derselben nicht wiederhergestellt werden können, sondern mit Daten gefüllt werden, die vom Benutzer oder aus sonstigen Quellen stammen. Beim Backup über iTunes oder die iCloud werden die im Documents-Verzeichnis liegenden Dateien gesichert, verlassen also das iDevice.

Abbildung

Abbildung 7.40 Die Sandbox des Fototagebuchs

Denken Sie beim Anlegen von Dateien in diesem Verzeichnis also gut darüber nach, welche Daten Sie dort ablegen, ob Sie diese Daten verschlüsseln müssen und ob die Daten das iDevice verlassen dürfen. Gerade im Unternehmensumfeld kann es aus rechtlichen Gründen sehr problematisch sein, wenn Daten ein iDevice in Richtung iCloud verlassen (siehe dazu auch Abschnitt 7.5, »iCloud«). In Abbildung 7.40 sehen Sie, dass sich die von Core Data erzeugte Datenbank Diary.sqlite im Documents-Verzeichnis der Sandbox befindet.

Genauso wie bei Mac OS X enthält das Library-Verzeichnis einer Sandbox alle Daten einer App, die keine Benutzerdaten enthalten. Die wichtigsten Unterverzeichnisse von Library sind die folgenden drei:

  • Application Support: Hier liegen alle Daten, die nicht im App-Bundle enthalten sind und die keine Benutzerdaten enthalten. Ein typisches Beispiel hierfür sind über In-App-Käufe bezogene Daten. iOS sichert die Daten in diesem Verzeichnis sowohl im iTunes- als auch im iCloud-Backup.
  • Caches: Das Caches-Unterverzeichnis von Library enthält Daten, die sich leicht wiederbeschaffen oder erzeugen lassen. Der Name des Verzeichnisses steht für den typischen Anwendungszweck: das Cachen. iOS sichert den Inhalt dieses Verzeichnisses nicht im Backup, und überdies löscht iOS seit der Version 5 den Inhalt des Verzeichnisses selbständig und ohne Vorwarnung, wenn der Speicherplatz auf dem Gerät knapp wird. Netterweise passiert das Löschen nicht, während die betroffene App aktiv ist, aber Sie müssen als Programmierer diesen Umstand unbedingt beachten, damit Sie (bzw. Ihre Nutzer) nicht Opfer eines unvorhergesehenen Datenverlustes werden. Der Umstand, dass der Inhalt des Cache-Verzeichnisses nicht ins Backup wandert, bedeutet auch, dass das Wiederherstellen eines iDevice zum Verlust aller Daten im Cache-Verzeichnis führt.
  • Preferences: Dieses Verzeichnis enthält die Einstellungen einer App. Die Empfehlung von Apple zu diesem Verzeichnis lautet, dort keine eigenen Dateien abzulegen, sondern die Einstellungen über NSUserDefaults (oder CFPreferences) zu speichern. Der Backup-Dienst sichert dieses Verzeichnis in iTunes und in der iCloud.

Beim Update einer App installiert iOS die neue Version in einer neuen Sandbox. Anschließend verschiebt es Dateien aus der Sandbox der alten App-Version in die neue Sandbox und löscht dann die alte Version der App. Dabei ist lediglich die Migration der Verzeichnisse Documents und Library garantiert.

Das tmp-Verzeichnis der Sandbox ist das, was der Name vermuten lässt: der Speicherort für temporäre Dateien, die nur zur Laufzeit einer App benötigt werden. iOS löscht im Bedarfsfall alte Dateien in diesem Verzeichnis; darüber hinaus hält Apple den Programmierer an, selbst dafür zu sorgen, dass eine App das Verzeichnis nicht unnötig vollmüllt. Der Inhalt dieses Verzeichnisses wird vom Backup nicht erfasst.

Das Bundle-Verzeichnis

Neben diesen Verzeichnissen existiert das Bundle-Verzeichnis, das in der Sandbox-Ansicht im Organizer nicht zu sehen ist. Dabei handelt es sich um ein Verzeichnis, dessen Name aus dem Namen der App und dem Postfix .app besteht, also z. B. PhotoDiary.app. Das Bundle-Verzeichnis ist digital signiert, und jede Änderung des Verzeichnisses durch Schreibvorgänge führt dazu, dass die Signatur ungültig wird. Infolgedessen startet iOS die App nicht mehr. Daher dürfen Sie nie Schreibvorgänge im Bundle-Verzeichnis vornehmen.

Wenn Ihre App Daten speichert, von denen Sie nicht möchten, dass sie ins Backup wandern (egal ob iTunes oder iCloud), dann bleibt Ihnen bei allen iOS-Versionen bis einschließlich 5 nichts anderes übrig, als die Daten entsprechend auf die oben genannten Verzeichnisse zu verteilen. Das ist natürlich nur ein billiger Workaround, und außerdem nicht zuverlässig, denn die Verzeichnisse, die nicht ins Backup wandern, garantieren keine durchgängige Verfügbarkeit der Daten. Die Apple-Dokumentation gibt diesbezüglich auch nur einen mäßig hilfreichen Tipp:

It is not possible to exclude data from backups on iOS 5.0. If your app must support iOS 5.0, then you will need to store your app data in Caches to avoid that data being backed up. iOS will delete your files from the Caches directory when necessary, so your app will need to degrade gracefully if it’s [sic] data files are deleted. [Anm.: http://developer.apple.com/library/ios/#qa/qa1719/_index.html]

Mit iOS 5.0.1 hat Apple die Möglichkeit eingeführt, ein erweitertes Attribut für Dateien zu vergeben, das dazu führt, dass die betreffenden Dateien nicht ins Backup wandern.

const char* dir= [[URL path] fileSystemRepresentation];
const char* srv = "com.apple.MobileBackup";
u_int8_t value = 1;
setxattr(dir, srv, & value, sizeof(value), 0, 0);

Listing 7.23 Unter iOS 5.0.1 können Sie Dateien vom Backup ausschließen.

Indem Sie mit Hilfe der C-Funktion setxattr den Wert für das Attribut com.apple.MobileBackup einer Datei auf 1 setzen, schließen Sie diese Datei vom automatischen Backup aus.

Aber auch das ist nur ein Workaround, denn mit iOS 5.1 hat Apple endlich den Parameter NSURLIsExcludedFromBackupKey für NSURL eingeführt, mit dem Sie eine URL vom Backup ausnehmen können. Damit ist die Funktion endlich im Foundation-Framework angekommen und dürfte ihren finalen Status erreicht haben. Glücklicherweise ist die Verbreitung älterer iOS-Versionen sehr gering, so dass der überwiegende Teil der Benutzer mit einer aktuellen Version arbeitet.

Um die von Core Data erzeugte Datei Diary.sqlite vom Backup auszuschließen, müssen Sie lediglich die Methode storeCoordinator im AppDelegate (PhotoDiaryAppDelegate.m) um die im Folgenden fett gedruckten Zeilen erweitern:

[...]
if ([theCoordinator addPersistentStoreWithType:
NSSQLiteStoreType configuration:nil
URL:theURL

options:nil error:&theError]) {
self.storeCoordinator = theCoordinator;
[[NSFileManager defaultManager]
fileExistsAtPath:[theURL path]];

BOOL result = [theURL setResourceValue:
[NSNumber numberWithBool: YES]
forKey:NSURLIsExcludedFromBackupKey error: &theError];
if(!result){
NSLog(@"Fehler beim Schützen der
Datenbank: %@ / %@", [theURL lastPathComponent], theError);
}
}
else {
NSLog(@"storeCoordinator: %@", theError);
}

Listing 7.24 Schutz vor automatischem Backup mit »NSURL«


Rheinwerk Computing - Zum Seitenanfang

7.4.9UDID ist tot. Was nun?Zur nächsten ÜberschriftZur vorigen Überschrift

Als App-Programmierer möchte (oder muss) man bisweilen einen Benutzer bzw. das Gerät eines Benutzers wiedererkennen können. Die Gründe dafür sind vielfältiger Natur. Der eine implementiert darüber einen Kopierschutz oder eine Authentisierung, ein anderer sammelt diese Informationen zu statistischen Zwecken. In den meisten Fällen resultiert das Verlangen nach einer eindeutigen Identifikationsmöglichkeit allerdings daraus, diese Daten zur Profilerstellung zu verwenden. Benutzerprofile können im besten Fall zur Optimierung von Apps verwendet werden, in der Regel werden sie aber zur Vermarktung an Werbetreibende erzeugt (Tracking oder Profiling).

Unter iOS gibt es verschiedene Möglichkeiten der Geräteidentifikation. Bis inklusive iOS 6 war es die bekannte UniqueDeviceID (UDID), die über die Klasse UIDevice ausgelesen werden konnte. Die UDID ist die weltweit eindeutige ID-Nummer eines iDevice. Mit iOS 5 hat Apple die UDID als deprecated gekennzeichnet, seit April 2013 ist das Auslesen der UDID ein Ausschlusskriterium für Apps im Apple Store, und mit iOS 7 ist die Methode zum Auslesen der UDID aus der API entfernt. Apple hat aber freundlicherweise gleichzeitig mit dem Abschaffen der UDID neue Möglichkeiten geschaffen, ein Gerät zu identifizieren.

UUID vs. UDID

iOS hält schon seit Version 2.0 die Funktion CFUUID bereit. Damit gibt das Betriebssystem eine UUID (Universal Unique Identifier) zurück. Diese UUID wird aber bei jedem Aufruf neu generiert und taugt daher nicht ohne weiteres zur Identifikation. Sie müssen sich als Programmierer selbst darum kümmern, die UUID zu speichern, z. B. in der Keychain, im Dateisystem oder in NSUserDefaults. Die UUID ist daher auch nur für eine einzige App gültig; eine andere App erhält beim Aufruf von CFUUID eine andere UUID. Das Gleiche gilt für die in iOS 6 hinzugekommene Klasse NSUUID. Damit sind beide Funktionalitäten zum Identifizieren von Geräten nicht hilfreicher als ein selbstgebauter Identifikationsstring und werden daher hier nicht näher beschrieben.

»advertisingIdentifier«

Für Werbetreibende mit einem Interesse an der Identifizierung eines Gerätes hat Apple im ApSupport-Framework den advertisingIdentifier implementiert, den Sie über die Klasse ASIdentifierManager abfragen können. Dazu müssen Sie lediglich die Headerdatei des AdSupport-Frameworks importieren und die Property advertisingIdentifier auslesen:

#import <AdSupport/ASIdentifierManager.h>
[...]
ASIdentifierManager *theManager = [ASIdentifierManager sharedManager];
NSUUID *theAdvertisingIdentifier = theManager.advertisingIdentifier;
NSLog(@"avertisingIdentifier: %@",
[theAdvertisingIdentifier UUIDString]);

Listing 7.25 Abfragen des advertisingIdentifier.

Die Konsole zeigt dann den advertisingIdentifier:

2013-07-20 23:08:46.066 Identifier[45299:a0b] 
avertisingIdentifier: 19BA604E-03BB-4F26-96C8-49DF16559226

Der Benutzer kann in den Einstellungen seines Gerätes angeben, dass er nicht getrackt werden möchte. Diese Einstellung lässt sich auch als Property von ASIdentifierManager auslesen:

theManager.advertisingTrackingEnabled

Es ist nicht so, dass iOS den Zugriff auf den advertisingIdentifier automatisch unterbindet, sobald ein Benutzer die Einstellung so gesetzt hat, dass er nicht getrackt werden möchte. Sie haben als Programmierer selbst die Verantwortung, den Wunsch des Benutzers zu respektieren, also die vorstehende Property auszulesen und mit der Erkenntnis etwas anzufangen.

Der advertisingIdentifier ist für alle Apps identisch. Damit unterscheidet er sich nicht wirklich von der alten, von Apple abgeschafften UDID. Im Gegensatz zur UDID kann der Benutzer aber den advertisingIdentifier zurücksetzen. Die Funktion befindet sich gut versteckt in den Einstellungen von iOS (EinstellungenDatenschutzWerbung). Dort können Sie auch einstellen, dass Sie kein Tracking wünschen (und darauf hoffen, dass sich irgendwer daran hält).

Abbildung

Abbildung 7.41 Einstellungen zum »advertisingIdentifier« in iOS 7.

Zurücksetzen des advertisingIdentifiers auf diesem Weg wirkt sich nur nach dem Neustart einer App aus.

Darüber hinaus wird der advertisingIdentifier beim Zurücksetzen des Gerätes neu generiert (EinstellungenAllgemeinZurücksetzen).

»identifierForVendor«

Neben dem advertisingIdentifier gibt es seit iOS 6 den identifierForVendor. Dabei handelt es sich auch um eine UUID, die für alle Apps desselben Herstellers (Vendor) gleich ist. Der Hersteller wird über die ersten beiden Teile des Bundle-Identifier definiert, also z. B. de.cocoaneheads. Der Hersteller steht in keinem Zusammenhang mit dem Entwicklerkonto, über das die App in den App Store eingestellt wurde.

Den identifierForVendor können Sie über die Klasse UIDevice abfragen, so wie eben die UDID:

[[[UIDevice currentDevice] identifierForVendor] UUIDString]);

Beachten Sie, dass der identifierForVendor nur für alle Apps desselben Herstellers auf einem Gerät identisch ist. Auf einem anderen Gerät erhalten die Apps desselben Herstellers einen anderen identifierForVendor. Löscht ein Benutzer alle Apps eines Herstellers auf seinem Gerät, wird bei der nächsten Installation einer App dieses Herstellers ein neuer identifierForVendor generiert. Wenn Sie den identifierForVendor also zu Analysezwecken verwenden, müssen Sie diesen Umstand berücksichtigen.

Der Unsinn mit der MAC-Adresse

Besonders aggressive Zeitgenossen haben seit längerem nicht nur die UDID ausgelesen, sondern die MAC-Adresse eines iDevice. Die MAC-Adresse, also die weltweit eindeutige Ethernet-Adresse der Netzwerkschnittelle ermöglicht eine sehr genaue Profilbildung. Dagegen wird jeder Supercookie im Webbrowser blass.

Apple hat das Problem erkannt und mit iOS 7 nicht nur die Methode zum Auslesen der UDID entfernt, sondern auch die Zugriffsmöglichkeit auf die MAC-Adresse. Die Apps, die unbefugt auf die MAC-Adresse eines Gerätes zugreifen, stehen seit iOS 7 also im Regen. Zu Recht.


Rheinwerk Computing - Zum Seitenanfang

7.4.10Base64Zur vorigen Überschrift

Bei der Übertragung von Netzwerkdaten und dem Umgang mit kryptographischem Schlüsselmaterial müssen Sie als Programmierer auf die Base64-Kodierung zurückgreifen. Base64 kommt überall dort zum Einsatz, wo nur eine ASCII-Kodierung möglich ist, die zu verarbeitenden Daten aber nicht im ASCII-Format vorliegen. Das können z. B. Binärdaten sein, die Sie im Netzwerk übertragen möchten, oder Schlüssel, die Sie an kryptografische Funktionen übergeben. Ein weiteres klassisches Beispiel für den Einsatz von Base64 ist das Versenden von E-Mails mit Anhängen im MIME-Format. Da das zum Versand von E-Mails verwendete SMTP-Protokoll nur den ASCII-Zeichensatz kennt, müssen alle mit diesem Protokoll zu versendenden Daten entsprechend konvertiert werden.

Seit iOS 4 gibt es eine private Methode der Klasse NSData zum Umgang mit Base64; seit iOS 7 ist diese Methode öffentlich, für alle iOS-Versionen größer als 4 jedoch deprecated. Das hört sich nicht nur verdreht an, es ist auch verdreht. Zusätzlich gibt es für iOS 7 aber auch zwei neue Methoden von NSData, die die Verarbeitung von Base64 erlauben: base64EncodedDataWithOptions zum Verarbeiten von Base64-kodierten Binärdaten und base64EncodedStringWithOptions zum Umgang mit Base64-kodierten Strings. Für beide Methoden gibt es noch Pendants zur Initialisierung eines NSData-Objektes: initWithBase64EncodedData:options und initWithBase64EncodedString:options.

Die Verwendung ist trivial. Das folgende Listing zeigt die Kodierung des Strings Foobar mit Base64 über die Methode base64EncodedStringWithOptions. Dazu wird zunächst ein NSString-Objekt erzeugt und ihm der String im Klartext zugewiesen. Anschließend wird ein NSData-Objekt erzeugt, das diesen String aufnimmt und in der dritten Zeile über die besagte Methode Base64-kodiert an ein weiteres Objekt vom Typ NSString übergibt.

NSString *foo = @"Foobar";
NSData *theData = [foo dataUsingEncoding:
NSUTF8StringEncoding];
NSString *bar = [theData
base64EncodedStringWithOptions:NSDataBase64Encoding64Character
LineLength];

Listing 7.26 Kodierung eines NSString mit Base64.

Um aus einem Base64-kodierten NSString wieder zu dekodieren, wird dieser String über die Methode initWithBase64EncodedString an ein NSData-Objekt übergeben, wo er dann im Klartext gespeichert ist:

NSData *theNewData = [[NSData alloc]  
initWithBase64EncodedString:bar
options:NSDataBase64DecodingIgnoreUnknownCharacters];

Listing 7.27 Base64-Dekodierung eines Strings.

Base64 und die Kryptografie

Bei Base64 handelt es sich um einen Kodierungsalgorithmus. Dieser kodiert die ihm übergebenen Daten, er verschlüsselt sie nicht. Das bedeutet, dass er lediglich die Darstellungsart ändert und dass jeder, der den Dekodierungsalgorithmus kennt, die Daten wieder dekodieren kann. Verwenden Sie Base64 also ausschließlich dazu, Daten zur Speicherung oder Übertragung zu kodieren, nicht um sie vor unbefugtem Zugriff zu schützen. Leider sieht man immer wieder selbstgebaute kryptographische Funktionen, die Daten mit Base64 kodieren und dabei unterstellen, dass diese Daten dann angemessen geschützt sind. Sind sie nicht! Zur Verschlüsselung taugen ausschließlich dafür vorgesehene kryptographische Algorithmen. Mehr dazu finden Sie in Abschnitt 7.5.4, »Verschlüsselung (in der Cloud)«.



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück




Copyright © Rheinwerk Verlag GmbH, Bonn 2014
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de


  Zum Katalog
Zum Katalog: Apps programmieren für iPhone und iPad






Neuauflage: Apps programmieren für iPhone und iPad
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Einstieg in Objective-C 2.0 und Cocoa






Einstieg in Objective-C 2.0 und Cocoa


Zum Katalog: Spieleprogrammierung mit Android Studio






Spieleprogrammierung mit Android Studio


Zum Katalog: Android 5






Android 5


Zum Katalog: iPhone und iPad-Apps entwickeln






iPhone und iPad-Apps entwickeln


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo