Tag Archives: iOS

“Borderline bulletproof” security for API keys

In episode 5 of my Keep in Touch podcast I shared some thoughts on how I currently think about protecting credentials, API keys, and secrets (summarised as keys for the rest of the post) for 1st and 3rd party services and APIs. This blog post is a recap of those ideas and also a call for feedback and input from you.

The risks

The problem I am talking about addressing, is related to the fact that many applications embed keys in their binaries. These keys are required to authenticate (and authorise) the app, the user, or both with remote services. These services could be public or private APIs. Similar considerations apply if these APIs are 1st party or 3rd party.

If these keys are bundled with the app, a nefarious attacker could extract them and then impersonate the developer.

It’s not my intention to teach bad actors how to execute their plans, but I do want to give a few examples of some attack vectors that could lead to sensitive information falling in the wrong hands:

  • the nightmare scenario: the attacker could run a man-in-the-middle attack by running a WiFi hotspot or could be operating one of the nodes on the internet between the device and the APIs the app connects to
  • the easier than you’d think scenario: the attacker could run an on-device proxy (such as the brilliant Charles for iOS app that I use daily for non-nefarious reasons), and monitor all traffic
  • the attacker could grab the app binary (ie. apk or ipa) and do a string search for key-like strings
    Any of the above could lead to the attacker obtaining the keys that the app uses to authenticate itself against remote servers. Once they get these keys they can start impersonating the app and its users.

The hypothesis

I believe that by not including any keys in the app’s binary and by provisioning these credentials only via a channel owned by the operating system that uses SSL pinning, the risks mentioned above can be reduced or even avoided.

SSL pinning is essential in order to prevent an attacker from inspecting the traffic between the client app and the remote endpoints.

Animated GIF showing the way keys can be securely transmitted from cloud to app to server. This flow is described in detail below

This animation attempts to illustrate the approach I am suggesting:

  1. Provision the app without any keys
  2. Attempt to detect if the app is running on a jailbroken / rooted device, and bail if that is the case
  3. Upload the necessary keys to the Trusted Cloud provided by the Host operating system (i.e. CloudKit for iOS apps) using another app (i.e. console, browser, etc)
  4. Enforce read-only access to these keys for the client app
  5. Upon (first) launch, fetch the keys from the Trusted Cloud
  6. If the keys must be stored, only store them in a keychain or in a secure enclave
  7. Use the keys to communicate with the Server using an SSL pinned connection
  8. Should the keys become compromised, they should be replaced, and the client apps should naturally fetch the new keys. For this reason, getting the new keys should not require the previous keys.

The steps above rely on the fact that the mobile OS provides a cloud service that uses SSL pinning in order to communicate to the client apps. Without it, there cannot be a secure key provisioning mechanism and the entire flow falls apart.

The checklist

Often times the security considerations come in “late” or “after the fact”. To assess the state of affairs, and to add more protection going forward, I would go over the checklist below:

  • Limit the number of people who have access to the credentials. Replace the credentials when these people leave the job.
  • Never embed the keys in the codebase
  • Never grant the build (or distribution) servers access to the keys
  • Only trust the cloud service that belongs to the platform vendor for your app (i.e. CloudKit for iOS apps, Firebase for Android apps.)
  • Avoid storing the keys anywhere (on the device) unless really necessary
  • If the keys must be stored, then the only acceptable place is the Keychain
  • Avoid bundling black-box libraries. If the libraries use networking, inspect all networking traffic
  • Do not use a 3rd party networking library unless absolutely necessary. Always check the source code of such libraries
  • Never be the largest consumer of any one technology, framework, or library
  • Only load the keys at the code level that really needs them. Do not pass them through the business layer, particularly if there are 3rd party analytics or logging libraries bundled with the app
  • Obfuscate the code and avoid obvious naming strategies for the keys
  • Check the app’s signature / hash or anything else similar that guarantees that the binary has not been tampered with
  • Be mindful of jailbroken or rooted devices but never change the code in order to support them
  • Do a quid-pro-quo vulnerability analysis with a trusted partner (make sure dinner or beer is something at stake if an issue is found)

If you have more to add to this list, I would love to hear from you!

Closing comments

I think of myself as a security minded person, but I am by no means an expert. I offer no guarantees that implementing the steps above is going to remove or prevent any security related problems for a system I have not seen before, but, based on my current knowledge, I do believe it is the closest thing to a “borderline bulletproof” approach to securing keys and credentials that native apps use.

Lastly, I sincerely hope that, if you read this and have identified a flaw in my reasoning, or if you can spot an oversight, you will share with me (ideally with a solution in tow) so I can update the article.

Homebridge, the secret sauce to making HomeKit awesome

As mentioned in the previous post, my HomeKit setup is made up of quite a few devices. Many of these are not HomeKit compatible out of the box; they are either too old, or the manufacturer chose not to add HomeKit support. Homebridge magically makes all these available in my ecosystem as if they came with 1st party HomeKit support.

Homebridge logo

Homebridge allows you to integrate with smart home devices that do not support the HomeKit protocol.

Installation

It may be scary for the non technical person out there, but it should be. Homebridge is surprisingly straightforward to set up. I did it on a (very old) Mac Mini and I found the documentation to be accurate and up to date. As it’s usually the case, developers strictly provide installation guides for their own work, rather than for all the dependencies. Luckily Homebridge only has one dependency (Node.js) so this steps should not take long for you to complete. (I recommend you follow the official instructions, but if you’re in a hurry, you can try the steps below)

Node.js

First check that you don’t already have node installed. Open a Terminal window and run the command below. You need to see a result that shows v4.3.2 or greater.

node --version

If you don’t have Node installed, proceed with the installation by downloading it from here. Follow the installation instructions and then run the command above one more time to make sure you’re ready to install Homebridge.

Homebridge

Installing Homebridge is relatively straightforward. Open the Terminal app and issue this command to install the node package:

$ sudo npm install -g --unsafe-perm homebridge

Still in Terminal, move to your home folder and try running homebridge. You’re very likely to see a message like the one below, followed by a QR code and a HomeKit code. Ignore these for now! 

$ homebridge
No plugins found. See the README for information on installing plugins.

Rather than adding the bridge to HomeKit, you should take the time to create your very own configuration:

$ nano ~/.homebridge/config.json

Depending on the devices / plugins that wish you to add, paste the following:

{
    "bridge": {
        "name": "Homebridge",
        "username": "CC:22:3D:E3:CE:30",
        "port": 51826,
        "pin": "989-98-989"
    },
    
    "description": "This is an example configuration file and you should make sure to replace this description and the pin info above with something else",

    "accessories": [
        {
            "accessory": "TV",
            "name": "Panasonic",
            "description": "Lounge TV",
            "ip": "192.168.1.100",
            "maxVolume": 13
        }, 
        {
            "accessory": "Onkyo",
            "name": "Receiver",
            "ip_address": "192.168.1.111",
            "model" : "TX-NR509",
            "poll_status_interval": "900",
            "default_input" : "sat",
            "default_volume": "35"
         }
    ],

    "platforms": [
        {
            "platform":"BelkinWeMo",
            "name":"WeMo Platform",
            "ignoredDevices":[  
            ]
        }
    ]
}

Feel free to replace the description above with whatever you like. Just make sure you keep the quotes and the file contains valid JSON. If in doubt, use a JSON validator website to check the config.

Important: change the pin number above with whatever number you wish that matches the pattern provided (3 digits, 2 digits, 3 digits).

The “name” you see above is quite useful: Siri will use that to perform actions on your device. For example, I can say “Hey Siri, turn off the Panasonic” and Siri will switch the TV off.

The accessories and platforms are the main types of plugins that you can add. Simple things are usually just an accessory. You’ll see that each homebridge-plugin has its own configuration snippet that you will end up adding to the config above.

In my case, to add the TV accessory above, I did the following things:

 $ npm install -g homebridge-panasonictv
  • I then added an accessory to my configuration file
{
       "accessory": "TV",
       "name": "TV",
       "description": "Livingroom tv",
       "ip": "192.168.178.20",
       "maxVolume": 15
}

If you’re not familiar with code or JSON, think of the “accessories” (or “platforms”) line as a parent for multiple accessories (platforms), that are separated between each other with a comma. Don’t forget to use the JSON validator website when you’re not sure you added your accessory or platform right. The square brackets indicate a collection or siblings (multiple accessories separated by commas). The curly brackets simple encapsulate an accessory. Pay attention to how the TV and Onkyo accessories are grouped together inside the “accessories” parent.

You can add as many individual accessories (platforms) as you wish but remember to install the corresponding plugins first.

To recap, I have installed node, homebridge, and some plugins. I then updated my config file with my own settings for the plugins I selected, so it’s time to finally start homebridge:

$ homebridge

If all the stars are aligned, you should now be looking at a QR code. Launch the Home app on your iOS device (and now even on your Mac), tap the + button and then select Add Accessory. Follow the instructions and pair the Home app with your Homebridge installation.

Homebridge screenshot from the Home iOS app

Congratulations, you now have a working HomeKit bridge that can breathe new life into old tech!

HomeKit, a mini series of posts describing my setup

I often find myself talking to friends about my HomeKit (home automation) set up, that I’ve been building up over the years. Quite often they are curios about aspects I now just take for granted.

For this reason I’m considering writing a mini series to cover how everything comes together.

I’m happy to take questions, so feel free to get in touch.

Next up: The main components in my HomeKit setup

Apple Wishlist from a New Zealand Customer

I live in Wellington, New Zealand and I own a truckload of Apple products. This is my wishlist of things I wish Apple would start doing.

iPhone & iPad

  • Visual Voicemail. I get that it may be up to the telcos, but I’d love to know that lots of pressure is being put on them to add support for this feature.
  • TV app
  • #images support for iMessage when the preferred language is Te Reo.

Apple Watch

  • LTE. Again, I realise it’s probably our telcos being slow to update their tech stack, but I wonder if they get the same lead time as their Australian counterparts.

Apple TV

  • TV app. Once you use it, it’s hard to go back to another interaction paradigm.
  • Siri support. Guess what, Siri understands us even when we use an Australian Apple Id and a cheap VPN. How come we have Siri on iOS, and watchOS, but not on tvOS? Even Siri on macOS works…

HomePod

  • Surely they know we use the same plug and voltage?!

Grab bag

  • Apple News. I am a registered Apple News Publisher, I can see Apple News in the Today View, so why can’t we have the News app?!
  • Public Transit support in Maps (thanks for Flyover, though).
  • Ability to purchase TV Shows.
  • … an Apple Store. Sending Macs and iOS devices to Sydney for repairs is super painful and not very well aligned with the Apple Green strategy. Authorised repair agents often can’t help (e.g Apple Watch is a no go).

What have I missed? What’s on your list? Head off to twitter to tell me how naïve I am.

Do you want to own your status updates?

After backing Manton Reece’s microblogging kickstarter I decided not to wait any longer and setup a MicroBlog section on my website where I would keep my short form updates. This post covers my goals and the approach I took to achieve these goals.

Goals

There are just a handful of things that I thought I’d need:

  • I own my content. I don’t mind posting to Twitter (or Medium) but the canonical location for my content is my own website
  • It’s easy. I can easily separate short form content (ie. statuses) and long form writing (ie. this post)
  • I still engage via Social Media. I can publish short form updates to my own website, and then the entries get cross-posted to Twitter
  • I  can post from my iPhone without needed to make edits from WordPress before publishing

Approach

I tried a few approaches (involving a range of apps such as IFTTT and various WordPress plugins) before I settled on the approach below.

It’s easy to own my content

My website is currently running on a self-hosted WordPress installation. There were two options here:

  1. I import all my Twitter posts under a special Category, or
  2. I post on my website first and then cross-post to Twitter

Option 2 feels more like “doing it right”, and, should anything go wrong in my setup, I never lose any posts I made from my own website.

What I decided to do was to:

  • create a new Category called MicroBlog
  • update the Menu to include this new category
  • replace the stock RSS widget with Category Specific RSS and use it to surface the MicroBlog RSS feed into the side bar
  • exclude the MicroBlog posts from the main page using the Ultimate Category Excluder plugin
  • make all posts in Category use the post format “status” so they looks consistent and timeline-like
  • remove all extra post decorations (ie. sharing) from the list of posts, but leave it on the post page itself
  • not use a post title, in order to mimic a status post more accurately

With these changes in place I ticked the first couple of boxes. What was left was to sort out cross posting to Twitter and publishing while on the go.

Sharing status updates to Twitter / Social Media

Posting to Twitter proved to be more difficult than I thought it would be. The obvious way, via Jetpack’s Publicize feature, seems to share only the link to the post when a title is not present. Therefore I had to look for other options, although I’d much prefer to just use Publicize.

The choice I settled on is an IFTTT rule: “when a new feed item is added to parfene.com/category/microblog/feed post a new tweet to @nicktmro”. The issue with this approach is that IFTTT doesn’t have a smart way of appending a URL to the post when it is over 140 characters, so I was “forced” to append a URL to the original post. It’s not very tidy but the counter argument is that it drives traffic (and search engines) back to the canonical location of the status update.

Posting while on the go

I carry an iPhone and a Pixel with me all the time. Posting updates to my website is a task I assigned to my iPhone.

I seek speed and simplicity when it comes to capturing my thoughts, which is why I use Drafts for almost every form of text capturing. This text sometimes ends up in iMessage, or in OmniFocus, or in an email, or in my Clipboard, or in WordPress… You get the idea.

I looked at the existing Drafts actions but I soon realised that I needed to be able to post exiting text and snippets, too. I needed an extension point. Enter Workflow. By delegating the communication with WordPress to Workflow I managed to increase the ways in which I drive content to my website.

Here’s how it all works:

  • I have a workflow that expects text input (or extracts it from the clipboard)
  • The workflow presets the WordPress category, post format, etc
  • This workflow is added as an action to Drafts

Now I’m good to go. When I choose to share my next status update all I do is go to my usual app (Drafts), write a snippet of text, and choose the Post to Parfene.com action.

Wrap up

I encourage you to try microblogging for yourself. My post is lengthy but you don’t have to do everything in one sitting. You don’t even need to fully automate everything, like I have.

This approach has made me feel more involved, engaged, and responsible with regards to the things I share with everyone. As somebody who oftentimes doesn’t count to five before he speaks, this should be a positive outcome…

 

 

TV Wishlist

I’ve owned an Apple TV for a few months now (I have a Dev Kit unit). I’ve coded for it and I’ve been enjoying it a lot more than I had anticipated. Here are some of the things I’d like to see in the upcoming iterations of the software / hardware.

Accessories for the USB-C Port

Camera

I’d buy a camera compatible with the this new Apple TV in an instant. I’d mostly use it for video chat (FaceTime, Skype, etc) but I can see how some other types of apps could use it too (Snapchat, Livestreaming a video podcast, etc).

Playroom / Kinect style games could also benefit from being able to plug in a video capture device.

Bluetooth is not an option because the camera would needs its own power source.

Game Accessories

This port could also be used for a number of game accessories. From Dance Dance Revolution mats, to Steering Wheels for your favourite driving game.

Web Browser

My reason may be different from yours. If you ever tried setting up an Apple TV in a hotel room, you probably know how annoying the whole log-in via a browser window situation can be.

Search APIs

Apps could be so much more powerful if they could tap into the Siri Remote search capabilities. Search is available to some video streaming services, but I think many more apps could benefit from such a feature. Here’s an example: “Siri, play a 10 minute summary of the new on Reuters”.

Picture-in-Picture

I’d love to be able to watch a film and then launch a Wikipedia or IMDB type app and interact with that app while the movie is still playing.

It would be great to be able to watch some live sports while shopping on Gilt, or browsing my next holiday destination on AirBnB. Anyone can get distracted during a game of cricket, surely.

New Remote

I’m one of those people who watch TV in the dark. Because of this I find myself holding the Siri remote “wrong”. I’m wishing for a less symmetrical remote that I can pick up an instantly know which way I’m holding. I also want a trackpad that is either less sensitive or is a bit smarter and doesn’t activate until I’m holding the remote with a tight grip or is able to detect accidental touches.

I’m hopeful

The reality is, that until the things above are implemented, the Apple TV will have to be shared with some other device. In my case, the couch device of choice is my iPad.

It’s likely that Apple prefers this, but they are also the one company that would be very happy to cannibalize its own product rather than wait for someone else to do it. Come on Apple TV, kick my iPad off the couch and turn it into my bed-side device.

3D Touch vs. Long Press

Apple announced the new iPhone 6S line at their September event. One of the tent poles of their new iPhones is 3D Touch, a hardware feature that provides “depth” to touch interactions. An argument can be made that this feature can be replicated by a Long Press (a gesture that is already available).

Interestingly enough, almost all the interactions demoed during the event could be built today, using a UILongPressGestureRecognizer. Even the taptic feedback could be faked with a vibration.

Here are some of the differences between the two gestures that I can think of.

  1. Resting a finger on the screen could mis-fire a Long Press, but not a 3D Touch
  2. 3D Touch can provide instant gratification. Long Press gestures are defined by a minimum touch delay, therefore they would be laggy in comparison
  3. It would be very difficult to implement an app with multiple Long Press gestures, but it would be straight forward to mix a Long Press gesture and 3D Touch
  4. Long Press does not provide depth. Games can benefit from using this feature

Neither 3D Touch, nor Long Press gestures are discoverable. That is potentially why we have not seen many Long Press implementations this far. Looking at the Apple Music app, the Long Press gesture (on a For You playlist for example) opens the overflow action menu. It will be interesting to see if 3D Touch replaces that, or augments it in some way. Even the Instagram implementation doesn’t really “need” 3D Touch to offer a preview of the selected photo. A one second tap could provide the same functionality, albeit with a bit of a delay.

I’m thinking of an analogy with the home button. Pushing it down twice takes the user to multi-tasking. Tapping it twice invokes the “reachability” which makes the screen slide down. I often forget that the latter feature even exists… Long pressing an app’s icon will compete with the gesture that invokes “wiggle” mode. It will be interesting to see if users will be confused by the two gestures.

I have a feeling that many developers will start implementing Long Press gesture fallbacks for 3D Touch and that more and more apps will start providing Peek + Pop behaviours in their applications. This alone can be a huge win for many users out there, as long as the developers don’t start hiding essential functionality behind a gesture that I believe is not very discoverable. (Did you know that a Long Press on the back button of OmniFocus takes you to the app’s home screen?)

Notes:
I have not used an iPhone with 3D Touch yet (pre-ordered mine yesterday) so many of the things above are just guesses.

The one fear I have is that the 3D Touch edge-screen-swipe multi tasking gesture will interfere with the back navigation gesture. I sure hope I’m wrong…

Thoughts on Notifications

Last Friday I was invited by the friendly people at Springload to give a talk on Push / Interactive Notifications.

The slides are targeted at Product people who are responsible with making the decision of including Push Notifications in the roadmap of their apps.

The gist of that talk is that just because you can send Push Notifications or display alerts to the user, it doesn’t mean you should. Notifications are the number one reason why people delete apps and you should keep this in mind when building your apps.

Here’s my (current) Top 10 Notification best practices:

  1. Guided “Opt In” rather than “Opt Out”
  2. Allow user to specify the types of messages they wish to receive. Support DND. Think Time Zone
  3. High volume of Notifications? Consider providing a “Snooze” custom action
  4. Only send relevant messages. This is NOT a direct marketing channel
  5. Don’t send confidential or sensitive data through push notifications
  6. Consider promoting custom actions that do not require the app to start up
  7. Use clear language and keep the message short
  8. Choose the lowest frequency of notifications that still delivers a great user experience
  9. Be aware of context. Is the user in your app right now?
  10. Consider aggregating multiple messages into a more generic “group”

Cocoaheads Wellington – Kick Off

I’m proud to announce the formation of Cocoaheads Wellington. For those who are not aware, Cocoaheads is an international Club for Cocoa (iOS & Mac) developers and designers.

The gatherings happen every 2nd Thursday of the month, from 7pm to 9pm. They are usually followed by a trip to a nearby restaurant or pub.

The first meeting will take place on the 12th of February at the Trade Me offices in Wellington. If you’re a Cocoa Dev / Designer then you should confirm your attendance here.

I intend to outsource the location. I’m currently hoping to convince one of the local Universities to host, but maybe another Wellington based organisation would be keen to put their hand up? If you know of any potential venues then please reach me via Twitter.

In terms of structure, I will propose when we gather that we break up the agenda in 3 parts:

  1. Follow up on the previous session
  2. Presentations (five to ten minutes each) on pre-agreed topics
  3. Q&A, tips or tricks, current issues or problems to ask the audience about

If you’re passionate about iOS or the Mac, you are keen to share your knowledge or learn from others, you live and breathe development or design, then I hope to see you there!

 

DevMob 2014 – Epilogue

This year’s DevMob was different compared to all the previous DevMobs: there was tangible equilibrium between iOS and Android, with both platforms showing maturity in terms of tools, design metaphors and developer attention.

Sponsors & Venue

DevMob2014

DevMob 2014 – Wellington

This (un)unconference1 would not have been possible without support from the sponsors: Microsoft, Alphero, Xero, Powershop, CactusLab, JSA, and 3 Months.

This year, the swag was particularly nice (thanks to the Alphero team). The sponsors also contributed with very interesting talks, availability and engagement throughout the weekend, and a wicked Lego Death Star draw!

It was Wellington’s turn to host, and the chosen venue was Samuel Marsden Collegiate School. I give it the thumbs up: the food was great, there were plenty of rooms (and each had a projector), and we were even allowed to use the amphitheatre.

An evolving conference

Over the years, the conference evolved from being focused on iOS only, to sharing the spotlight between iOS and Android. This reflects the marketshare and the user base of the two platforms, and the ever growing number of developers interested in them. As one of the attendees who hasn’t missed a single iteration of this event, I’m happy to see such organic growth. It’s also good to see that the Microsoft folk have not given up, and keep attending these gatherings and contributing with incredibly useful insights (Azure being one such example).

My observation (potentially wrong) is that, year after year, the iOS talks have become a bit more abstract and more high level (a good incentive for me to return). At the same time, the audience has managed to keep some practical sessions on the board:

  • the show-and-tell sessions took a UI/UX spin
  • the demos contained products that are more and more polished (even though their authors consider them “rough”)
  • the live coding sessions were focused on concepts that a newcomer would probably struggle to fully understand, but would still find valuable

If a couple of years ago Julius was struggling2 to attract a handful of devs to take part in a blue-sky discussion, this year all the Android sessions had full rooms. Refreshingly, Google’s Material Design was at the centre of most discussions. The tools and libraries that people spoke about covered everything you can possibly think of: from analytics, networking, and storage, all the way to dependency injection and reactive programming.

I’m sad to see that Android developers still have to battle with basic things such as networking and resource loading. Michael Rueger from Xero gave a very good talk about the way their app tackles this area. I definitely think his talk should be shared at one of the next Wellington Android Development Meetups. While I’m talking about Xero, Glenn Parker was particularly active and even shared some interesting work they have underway. The Poweshop folks also contributed a great amount, confirming that Wellington is the place to be for Android developers. Not to be outdone, I shared as much as I could about the way that we do things at Trade Me.

No doubt about it, Android is a first class citizen and there’s some outstanding work being done in this space.

New in 2014

Back in 2009 when Jade Software launched DevMob (under the NZiDev moniker), the questions that people used to ask were along the lines of “how do I code, test, deploy, market my app, all at the same time?”. After just 5 editions, this question has transformed into “how do I run a distributed team? how do we collaborate with designers and testers? how do you manage an app’s backlog?”.

This year, I believe two themes were new to the stage: mobile development at scale, and an in-depth look at how to build mobile software. The gimmicky apps were pretty much absent, confirming that the mobile gold rush is well and truly over. For a change, Layton and Karl did not have to too many questions on how to build the next million dollar app.

Scale

Surprisingly, managing scale was probably one of the most talked about things this year. So much so that the group ended up creating a follow-up session after Luke Gumbley‘s “Backlog management / Feature planning” talk.

Team building, maintaining happiness, and dealing with ownership, all got tackled by the audience. The information flowed freely and openly thanks to the trusted environment that makes so many people return to this event year after year.

Depth

It wasn’t just the management side of things that was new this year. The sessions went deeper into the product life-cycle with topics such as API design, security, functional testing and mocking, UX, continuous integration and more.

I was happy to see large development houses (Cactuslab, JSA, Powershop, Trade Me, Vend, Xero, etc) share intimate details about how they tackle many aspects of the SDLC. The fact that non-coding topics permeated DevMob 2014 is encouraging for future iterations of this (un)conference.

Until next time

I missed out on the socialising that traditionally happens after the first day of the conference. I suppose that’s part of the deal when the event is hosted in one’s home town.

Many thanks to the hosts Nat, Janine, TanyaJulie and everyone who contributed to making this event a success.

If I got anything wrong, or if you wish to pass any feedback, you can reach me on twitter.

Notes:
1 To learn more about (un)conferences you can visit Nat’s website, buy this Kindle book, or, my favourite option, just take part in one!
2 Julius is no longer struggling to attract new people to his talks. He now just needs to find a way to keep Sam awake. (Okay, this photo is cropped and you can’t see the 30 people sitting behind Sam, but it’s funny nonetheless.)

Julius teaches Sam Material Design

Julius teaches Sam Material Design