Coder Social home page Coder Social logo

fxkeychain's Introduction


WARNING: THIS PROJECT IS DEPRECATED

It will not receive any future updates or bug fixes. If you are using it, please migrate to another solution.


Purpose

FXKeychain is a lightweight wrapper around the Apple keychain APIs that exposes the commonly used functionality whilst hiding the horrific complexity and ugly interface of the underlying APIs.

FXKeychain treats the keychain like a simple dictionary that you can set and get values from. For most purposes you can get by using the defaultKeychain, however it is also possible to create new keychain instances if you wish to namespace your keychain by service, or share values between apps using an accessGroup.

Supported iOS & SDK Versions

  • Supported build target - iOS 8.0 / Mac OS 10.9 (Xcode 6.0, Apple LLVM compiler 6.0)
  • Earliest supported deployment target - iOS 5.0 / Mac OS 10.7
  • Earliest compatible deployment target - iOS 4.3 / Mac OS 10.6

NOTE: 'Supported' means that the library has been tested with this version. 'Compatible' means that the library should work on this iOS version (i.e. it doesn't rely on any unavailable SDK features) but is no longer being tested for compatibility and may require tweaking or bug fixes to run correctly.

ARC Compatibility

FXKeychain requires ARC. If you wish to use FXKeychain in a non-ARC project, just add the -fobjc-arc compiler flag to the FXKeychain.m class. To do this, go to the Build Phases tab in your target settings, open the Compile Sources group, double-click FXKeychain.m in the list and type -fobjc-arc into the popover.

If you wish to convert your whole project to ARC, comment out the #error line in FXKeychain.m, then run the Edit > Refactor > Convert to Objective-C ARC... tool in Xcode and make sure all files that you wish to use ARC for (including FXKeychain.m) are checked.

Installation

To use FXKeychain, just drag the class files into your project and add the Security framework. You can use the [FXKeychain defaultKeychain] shared instance, or create new instance as and when you need them.

Thread Safety

  1. It is safe to use a given FXKeychain instance from any thread.
  2. Use a single FXKeychain instance per thread, do not access a single instance from more than one thread concurrently (including the default instance).
  3. If you have multiple FKKeychain instances that point to the same service, it is safe to read their values from multiple threads concurrently, but you should not attempt to write to the same key from two different threads concurrently.

Security

Caution is advised when storing and retrieving non-string objects from the keychain. On iOS, the keychain is sandboxed to a single app or to a group of apps shared by a single developer. But on Mac OS, any app can read or write to any entry in the keychain. This offers the potential for a malicious app to attempt to manipulate the behaviour of another by changing its keychain data.

Version 1.2 and earlier of FXKeychain allowed arbitrary classes to be stored in the keychain using NSCoding. This feature was removed in 1.3 to mitigate the risk that an app might change the encoded classes in your app's keychain in order to get it to load and run code that it isn't supposed to. In version 1.5, the feature has been restored but is controlled by the FXKEYCHAIN_USE_NSCODING macro. It is enabld by default on iOS (which is sandboxed and therefore relatively safe) and disabled by default on Mac OS (which isn't).

Code injection is a low risk on iOS (unless the device is jailbroken). On Mac OS, using version 1.3 or above should protect you from code injection, as only plist-compatible classes are now supported by default, which cannot easily be used in a malicious way. It is still recommended however that you verify that the data being loaded from the keychain matches the type and structure that you are expecting in order to protect against malicious or mischevious tinkering with the data that might crash your app or cause it to behave strangely.

Properties

FXKeychain has the following properties:

@property (nonatomic, copy, readonly) NSString *service;

The service property is used to distinguish between multiple apps or services on a given device or within the same app. On Mac OS and the iOS simulator, services are shared between apps, so it's a good idea to use something unique for the service, such as the application bundle ID, or the same value as the accessGroup if you wish to share a service between multiple apps. The service value cannot be changed after the keychain has been created.

@property (nonatomic, copy, readonly) NSString *accessGroup;

The accessGroup property is used for sharing a keychain between multiple iOS apps from the same vendor. See Apple's documentation for acceptable values to use for the accessGroup. Leave this value nil if you do not intend to share the keychain between apps. On Mac OS, the keychain is already shared between apps, so this property has no effect. The accessGroup cannot be changed after the keychain has been created.

@property (nonatomic, assign) FXKeychainAccess accessibility;

The accessibility property is used for controlling access to the keychain when the device is locked. See FXKeychainAccess values description below for possible values. On Mac OS, prior to 10.9 (Mavericks) this property has no effect. Unlike the other attributes, the accessibility property can be changed at any time, however, changes will only affect keys that are set subsequent to the change; existing keys in the keychain will not be affected unless they are re-written.

Methods

+ (instancetype)defaultKeychain;

This method returns a shared default keychain instance, which uses the app's bundle ID for the service to avoid namespace collisions with other apps on Mac OS or the iOS simulator.

- (id)initWithService:(NSString *)service
          accessGroup:(NSString *)accessGroup;

- (id)initWithService:(NSString *)service
          accessGroup:(NSString *)accessGroup
        accessibility:(FXKeychainAccess)accessibility;

This method creates a new FXKeychain instance with the specified parameters. Each FXKeychain can contain as many key/value pairs as you want, so you probably only need a single FXKeychain per application. Each FXKeychain is uniquely identified by the service parameter; see the Properties description for how to use this. You can specify nil for the service, in which case it will act as "wildcard" selector and calls to objectForKey: will return the first value found within any service stored in the keychain. The accessGroup parameter is used for setting up shared keychains that can be accessed by multiple different apps; leave this as nil if you do not require that functionality. The optional accessibility property controls whether the keychain items can be accessed if the app is launched in the background when the device is locked (see FXKeychainAccess values description below for details).

- (BOOL)setObject:(id)object forKey:(id)key;
- (BOOL)setObject:(id)object forKeyedSubscript:(id)key;

These methods will save the specified object in the keychain. Any plist-compatible object (NSDictionary, NSArray, NSString, NSNumber, NSDate, NSNull) can be stored. Objects of type NSString will be stored as UTF8-encoded data, and are intercompatible with other keychain solutions. Any other object type will be stored using binary plist encoding. Passing a value of nil as the object will remove the key from the keychain. Passing an object of any other type (or a collection containing an object of any other type) will throw an exception. The second form of this method is functionally identical to the first, but is included to support the modern Objective-C keyed subscripting syntax.

- (BOOL)removeObjectForKey:(id)key;

This method deletes the specified key from the keychain.

- (id)objectForKey:(id)key;
- (id)objectForKeyedSubscript:(id)key;

This method returns the value for the specified key from the keychain. If the key does not exist it will return nil. The second form of this method is functionally identical to the first, but is included to support the modern Objective-C keyed subscripting syntax.

FXKeychainAccess values

FXKeychainAccessibleWhenUnlocked

This is the default value. Keychain items set with this accessibility level can only be accessed when the device is unlocked. If your app needs to access the keychain when running in the background, this may cause problems.

FXKeychainAccessibleAfterFirstUnlock

Keychain items set with this accessibility level can be accessed once the keychain has been unlocked, and will remain accessible until the device is restarted, even if the device is locked again in the meantime. This is a good choice for items that need to be accessed by background services.

FXKeychainAccessibleAlways

Keychain items set with this accessibility level can be accessed at any time. This isn't very secure compared with the other options, but it's still better than storing values in plain text in the file system!

FXKeychainAccessibleWhenUnlockedThisDeviceOnly
FXKeychainAccessibleAfterFirstUnlockThisDeviceOnly
FXKeychainAccessibleAlwaysThisDeviceOnly

These values behave the same way as their non-ThisDeviceOnly counterparts, except that they are not backed up and restored if the device is reset or upgraded, and are therefore more secure (but also less reliable).

Release Notes

Version 1.5.3

  • Fixed crash when stored value is an array

Version 1.5.2

  • Fixed issue on iOS 8 that may have caused crashes and made accessGroup not work correctly

Version 1.5.1

  • No longer logs a warning if you attempt to delete a key that doesn't exist

Version 1.5

  • The accessibility property is now readwrite, allowing you to change accessibility on a per-property basis. Note that changing the value will only affect keys that are set subsequent to the change.
  • NSNull values are now stripped when saving if NSCoding is disabled, avoiding a possible cause of encoding failure in otherwise valid code
  • Restored support for NSCoding, but this is enabled by default on iOS only. You can enable it for Mac OS using a precompiler macro, but this is not recommended for security reasons
  • Suppressed some console warnings that would occur if password contained an = character
  • Now complies with -Weverything warning level

Version 1.4

  • Added access parameter for optionally allowing keychain access when device is locked

Version 1.3.4

  • Fixed bug where passwords containing certain special characters could be wrongly interpreted as a property list when loading
  • Added code to prevent injection attacks based on users supplying a password containing binary plist data

Version 1.3.3

  • Fixed issue with deleting keychain items on Mac OS

Version 1.3.2

  • Now throws an exception if you try to encode an invalid object type instead of merely logging to console

Version 1.3.1

  • Fixed singleton implementation

Version 1.3

  • Removed ability to store arbitrary classes in keychain for security reasons (see README). It is still possible to store dictionaries, arrays, etc.

Version 1.2

  • It is now possible to actually store more than one value per FXKeychain
  • Removed account parameter (it didn't work the way I thought)

Version 1.1

  • Now uses application bundle ID to namespace the default keychain
  • Now supports keyed subscripting (e.g. keychain["foo"] = bar;)
  • Included CocoaPods podspec file
  • Included Mac OS example

Version 1.0

  • Initial release

fxkeychain's People

Contributors

14lox avatar ashton-w avatar fcy avatar indragiek avatar javisoto avatar nicklockwood avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fxkeychain's Issues

Clear keychain after reinstall

Today I've got a problem that even after reinstalling the app I get FXKeychain already saved previous options. On the first load,
FXKeychain *keychain = [FXKeychain defaultKeychain];
i get
[keychain objectForKey:kUserGUID] not nil,
I get a previous value somehow. I'm not sure where the problem is. If someone still using this pod, any help would be appreciated.
But I see the last commit is about 2 years ago, probably should just stop using it, maybe some parts are just deprecated for current ios version.

Old-style plist parser

Hi whenever i set or get data with key i'l get this message in console:

CFPropertyListCreateFromXMLData(): Old-style plist parser: missing semicolon in dictionary on line 1. Parsing will be abandoned. Break on _CFPropertyListMissingSemicolon to debug.

i use last version of keychain(1.4) on iOS (7.1b4)

Setting the accessibility of the default keychain

As a follow up to #8, is there any reason why the accessibility property is readonly?

Shouldn't I be able to set it on the default keychain as long as I set it before I assign any values in the keychain?

Expected a type / Unknown property attribute 'nullable' errors

Cocoapods recently updated to 1.5.3 and I started getting the attached errors:
screen shot 2015-05-25 at 2 01 52 am

I'm unfamiliar with nullable or nonnull. What is going on?

Also, 1.5.2 worked well but I can't get back to it even when explicitly asking for:
pod 'FXKeychain', '~> 1.5.2'.

dataWithPropertyList returns nil on NSDictionary

This line https://github.com/nicklockwood/FXKeychain/blob/master/FXKeychain/FXKeychain.m#L141

is returning nil for when object is a standard NSDictionary on OS X 10.9, and the NSAssert that comes afterwards kicks in.

I don't believe there's anything special in the NSDictionary, 21 key/value pairs, gets saved without issues on an iOS keychain with another library.

The code seems correct, so I'm wondering what can be wrong, still investigating, if you have any idea I'll be happy to try.

NSCoding encodeWithCoder: not being called inside setObject:forKey:

Hi! I am using a custom class which conforms to NSCoding. I am getting an error + assertion inside setObject:forKey:

NSAssert(!object || (object && data), @"FXKeychain failed to encode object for key '%@', error: %@", key, error);

And [NSPropertyListSerialization propertyListWithData:options:format:error] produces this error:

Property list invalid for format: 200 (property lists cannot contain objects of type 'CFType')

My class conforms to NSCoding so I put a breakpoint on encodeWithCoder: and noticed that encodeWithCoder: is never being called. Any ideas what I am doing wrong?

Thanks!

Shared Keychain iOS 8 Simulator on Yosemite

There is an issue using shared keychain items only in Yosemite using iOS 8 simulators. It appears that the kSecAttrAccessGroup is now needed in this specific configuration. Keychain calls that would previously fail when including the kSecAttrAccessGroup key with an error of -25243 are now successful. Further, the shared keychain does not appear to be globally available to the simulator requiring the kSecAttrAccessGroup key to be used. I simply commented out the simulator check in dataForKey: and setObject:forKey:

//#if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR
    if ([_accessGroup length]) query[(__bridge NSString *)kSecAttrAccessGroup] = _accessGroup;
//#endif

I haven't figured out any other way around this issue yet.

Update: I'm using OS X Yosemite v10.10.1 (14B25) and Xcode 6.1 (6A1052d)

fail to store

Hi guys

I'm trying to use FXKeychain (used another one before, but this one seemed very solid ;p ).

When running on my device (iOS8 b4), I keep getting this error message :

FXKeychain failed to store data for key 'kPasswordKeychainKey', error: -25243

but it work just fine on the simulator. I tried to add Data protection capabilities, it generated the entitlement.plist file, but I can't get to to work on my device.

Any idea?
thanks.

classes not properly 'inflating'

(on iOS) my class is loading from a file and when I try and access a property i'm getting an error about NSDictionary being "not key value coding-compliant for the key XXX". While i can see that at the time of the crash the instance of the object is in fact an NSDictionary, my perception is that it should actually inflate as the class that it was saved as.

my class is an instance of BaseModel, and was previously using BMFileFormatKeyedArchive (which works), but this particular class that i'm saving I'd like to make more secure.

Can't save multiple key/values to the same keychain

I tried using FXKeyChain to save different items to the keychain but I was getting errors. To show this I patched the basic example with the code at gradha@f590521 which shows saving three specific values after the one specified by the user in the example. These values are not saved, as displayed by the following log:

2013-03-21 19:23:13.632 FXKeychainExample[12469:c07] FXKeychain failed to store data for key '1', error: -25299
2013-03-21 19:23:13.645 FXKeychainExample[12469:c07] FXKeychain failed to store data for key '2', error: -25299
2013-03-21 19:23:13.649 FXKeychainExample[12469:c07] FXKeychain failed to store data for key '3', error: -25299

Also, if I press the save button on the unmodified example, change the name of the key to something else and press save again, I get a similar error:

2013-03-21 19:24:23.173 FXKeychainExample[12469:c07] FXKeychain failed to store data for key 'passw', error: -25299

This seems to be contrary to the README where it says Each FXKeychain can contain as many key/value pairs as you want, so you may only need a single FXKeychain per application. Can you tell what is wrong with these modifications? Maybe keychain access changed in iOS 6 devices and you have to create one instance for key/value?

Error with iOS8 & XCode 6

Any saving within your basic "FXKeychainExample" app results in "FXKeychain failed to store data for key 'password', error: -34018" - Works just fine in XCode 5/iOS 7.0/7.1

This error occurs when running in simulator. It works fine on an actual device. Any clue what the issue may be?

Support for Keychain Item Accessibility Constants

When adding a keychain item through SecItemAdd, one can specify the kSecAttrAccessible to specify when a keychain item should be readable.

I believe that if the kSecAttrAccessible is not specified, the default protection class is kSecAttrAccessibleWhenUnlocked, which means that the keychain item can only be accessed while the device is unlocked.

However, there are cases where caller would like to specify the keychain item to be readable after first unlock (kSecAttrAccessibleAfterFirstUnlock and kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly). This is especially true with iOS 7 where new multi-tasking APIs are added to support background fetch and download/upload. During these background operations, device could be in the locked state and keychain items might be needed for authentication among other things.

Possible to set accessibility in app delegate

Are there any issues with setting the accessibility level in the app delegate applicationDidFinishLoading??

[FXKeychain defaultKeychain].accessibility = FXKeychainAccessibleAlways;

Getting last status

When getting a value from the keychain fails, it would be nice if it was possible to get the last status result. Was it errSecItemNotFound or errSecInteractionNotAllowed, for instance?

QUERY: allKeys?

i don't know if this makes sense or not: how about adding a method like:

    - (NSArray *)allKeys

that returns all the keys in a keychain?

i'm using FXKeychain to keep track of N servers, and i'd like to be able to present a list to the user of servers that they have keys for.

Add "description" method

It would be nice to have a "description" method implemented to list all the keychain entries like in a UICKeyChainStore.

I suppose that move info.plist out of FXKeychain folder

I suppose that move info.plist out of FXKeychain folder. Because I suffered from that info.plist "no such file", and I checked my project over and over again and my info.plist is there, but when I start to complie, Xcode 7.0 always warn me about that the file “Info.plist” couldn’t be opened because there is no such file.
. It about to drive me crazy, and I have to work with my project. So I changed my Xcode version to 6.4, A Ha, Finally it says more like that "Keychains/FXKeychain/FXKeychain/Info.plist:0: " , I finally got that I forget to move the FXKeychain folders to my project . If only I used the package manager to make integration.

Updating key fails in simulator with ios 7.1B5 (works fine on device)

Don't think this is an FXKeyChain issue but for info - updating a key fails with a -25300 error (errSecItemNotFound).

Sample code (as per radar which is a cut down version of FXKeyChain attached below)

/// ---------------------------------------------------------

//
//  ViewController.m
//  KeychainBugSample
//
//  Created by Andy Qua on 12/02/2014.
//  Copyright (c) 2014 Andy Qua. All rights reserved.
//

#import "ViewController.h"


/*******
 * Sample demonstrating a bug with updating a value stored in the KeyChain.
 * When running on Simulator this fails with a -25300 error (errSecItemNotFound)
 * But works fine when running on device
 *
 * Note this is simplified keychain code and assumes that you will be saving and retrieving an NSString value
 *******/

@interface ViewController ()
{
    NSString *service;
}
@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    service = @"test";
    NSString *key = @"MyKey";
    NSString *text;

    // Key shouldn't exist
    text = [self stringForKey:key];
    NSLog( @"Before add - value stored - [%@]", text );

    // Set value
    [self addString:@"Test" forKey:key];

    // Make sure that value has been set correctly
    text = [self stringForKey:key];
    NSLog( @"After initial add - value stored - [%@]", text );

    // Now Update the value - This will log error  -25300 (keyNotFound)
    [self updateString:@"This has changed" forKey:key];

    // Make sure that value has been set correctly
    text = [self stringForKey:key];
    NSLog( @"After update value - [%@]", text );

    // Clear outs data from keychain for next run
    [self deleteDataForKey:key];
    text = [self stringForKey:key];
    NSLog( @"After delete value - [%@]", text );
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


- (NSString *)stringForKey:(id)key
{
    //generate query
    NSMutableDictionary *query = [NSMutableDictionary dictionary];
    if ([service length]) query[(__bridge NSString *)kSecAttrService] = service;
    query[(__bridge NSString *)kSecClass] = (__bridge id)kSecClassGenericPassword;
    query[(__bridge NSString *)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
    query[(__bridge NSString *)kSecReturnData] = (__bridge id)kCFBooleanTrue;
    query[(__bridge NSString *)kSecAttrAccount] = [key description];

    //recover data
    CFDataRef data = NULL;
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&data);
    if (status != errSecSuccess && status != errSecItemNotFound)
    {
        NSLog(@"Failed to retrieve data for key '%@', error: %ld", key, (long)status);
    }

    NSData *dataObj = CFBridgingRelease(data);
    NSString *text = [[NSString alloc] initWithData:dataObj encoding:NSUTF8StringEncoding];

    return text;
}


- (BOOL) addString:(NSString *)string forKey:(NSString *)key
{
    NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];

    NSMutableDictionary *query = [NSMutableDictionary dictionary];
    if ([service length]) query[(__bridge NSString *)kSecAttrService] = service;
    query[(__bridge NSString *)kSecClass] = (__bridge id)kSecClassGenericPassword;
    query[(__bridge NSString *)kSecAttrAccount] = [key description];
    query[(__bridge NSString *)kSecValueData] = data;

    OSStatus status = SecItemAdd ((__bridge CFDictionaryRef)query, NULL);

    if (status != errSecSuccess)
    {
        NSLog(@"Failed to add data for key '%@', error: %ld", key, (long)status);
        return NO;
    }

    return YES;
}

- (BOOL) updateString:(NSString *)string forKey:(NSString *)key
{
    NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];

    NSMutableDictionary *query = [NSMutableDictionary dictionary];
    if ([service length]) query[(__bridge NSString *)kSecAttrService] = service;
    query[(__bridge NSString *)kSecClass] = (__bridge id)kSecClassGenericPassword;
    query[(__bridge NSString *)kSecAttrAccount] = [key description];

    NSMutableDictionary *update = [@{(__bridge NSString *)kSecValueData: data} mutableCopy];

    OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);

    if (status != errSecSuccess)
    {
        NSLog(@"Failed to update data for key '%@', error: %ld", key, (long)status);
        return NO;
    }

    return YES;
}

- (BOOL)deleteDataForKey:(id)key
{
    //generate query
    NSMutableDictionary *query = [NSMutableDictionary dictionary];
    if ([service length]) query[(__bridge NSString *)kSecAttrService] = service;
    query[(__bridge NSString *)kSecClass] = (__bridge id)kSecClassGenericPassword;
    query[(__bridge NSString *)kSecAttrAccount] = [key description];

    //delete existing data

    OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
    if (status != errSecSuccess)
    {
        NSLog(@"Failed to delete data for key '%@', error: %ld", key, (long)status);
        return NO;
    }

    return YES;
}

@end
/// ---------------------------------------------------------

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.