GPG with Confirmation

I got tired of Evolution email client giving me those horrid error messages when ever I try to email someone who’s key isn’t in my current list of keys.

The design of this is appallingly bad. It discourages the use of GPG rather than encouraging the importing of keys and it makes no mention of helping you acquire keys if possible. It also allows for no additional or optional footer to explain to the recipient that their message couldn’t be encrypted because they don’t use GPG.

While I couldn’t do much about the later, without hacking on the evolution codebase directly. I did do a bit of hacking on the former with a gpg middlware. Yes, when I say hack, I mean HACK. A dangerous and potentially devastating way of wrapping the gpg binary with my own python script that could intercept the evolution call and do work to search, display and add keys to encourage the use of encryption overall.

The design was simple. When we are asked to encrypt for a person who we don’t have the keys for, we do a search. The results are shown in a GUI to the user and they can select a key to use. This then is added to the key ring and used to encrypt the email.

This setup allows for experimentation with user prompting and workflow. It’s not something I would recommend be installed on user’s computers. But for designers and developers, this sort of match-stick making is a valuable platform to build, try, test and rebuild quickly.

I use zenity for the user interface. This is a Gtk command line tool that lets you launch a window from the command line and the interface is good enough to support photos in lists and returning which item was selected. Very cool.

Bellow you will find the script I created for this hack, this is saved to /usr/bin/gpg and gpg is moved to gpg.orig:

#!/usr/bin/python
#
# Wrap the gpg command to provide evolution with a bit of extra functionality
# This is certainly a hack and you should feel very bad about using it.
#
# Public Domain, Authored by Martin Owens  2016
#
import os
import sys
import atexit

from collections import defaultdict
from subprocess import Popen, PIPE, call
from tempfile import mkdtemp, mktemp
from datetime import date
from shutil import rmtree

to_date = lambda d: date(*[int(p) for p in d.split('-')])


class GPG(object):
    keyserver = 'hkp://pgp.mit.edu'
    remote_commands = ['--search-keys', '--recv-keys']

    def __init__(self, cmd='/usr/bin/gpg', local=False):
        self.command = cmd
        self.photos = []
        self.local = local
        self.homedir = mkdtemp() if local else None
        atexit.register(self.at_exit)

    def at_exit(self):
        """Remove any temporary files and cleanup"""
        # Clean up any used local home directory (only if it's local)
        if self.local and self.homedir and os.path.isdir(self.homedir):
            rmtree(self.homedir)

        # Clean up any downloaded photo-ids
        for photo in self.photos:
            if os.path.isfile(photo):
                os.unlink(photo)
            try:
                os.rmdir(os.path.dirname(photo))
            except OSError:
                pass

    def __call__(self, *args):
        """Call gpg command for result"""
        # Add key server if required
        if any([cmd in args for cmd in self.remote_commands]):
            args = ('--keyserver', self.keyserver) + args
        if self.homedir:
            args = ('--homedir', self.homedir) + args

        command = Popen([self.command, '--batch'] + list(args), stdout=PIPE)
        (out, err) = command.communicate()
        self.status = command.returncode
        return out

    def list_keys(self, *keys, **options):
        """Returns a list of keys (with photos if needed)"""
        with_photos = options.get('photos', False)
        args = ()
        if with_photos:
            args += ('--list-options', 'show-photos',
                     '--photo-viewer', 'echo PHOTO:%I')
        out = self(*(args + ('--list-keys',) + keys))

        # Processing the output with this parser
        units = []
        current = defaultdict(list)
        for line in out.split('\n'):
            if not line.strip():
                # We should always output entries if they have a uid and key
                if current and 'uid' in current and 'key' in current:
                    # But ignore revoked keys if revoked option is True
                    if not (current.get('revoked', False) and options.get('revoked', False)):
                        units.append(dict(current))

                current = defaultdict(list)

            elif line.startswith('PHOTO:'):
                current['photo'] = line.split(':', 1)[-1]
                self.photos.append(current['photo'])
            elif ' of size ' in line:
                continue
            elif '   ' in line:
                (kind, line) = line.split('   ', 1)
                if kind == 'pub':
                    current['expires'] = False
                    current['revoked'] = False

                    if '[' in line:
                        (line, mod) = line.strip().split('[', 1)
                        (mod, _) = mod.split(']', 1)
                        if ': ' in mod:
                            (mod, edited) = mod.split(': ', 1)
                            current[mod] = to_date(edited)

                    (key, created) = line.split(' ', 1)
                    current['created'] = to_date(created)
                    (current['bits'], current['key']) = key.split('/', 1)
                elif kind in ('uid', 'sub'):
                    current[kind].append(line.strip())
                else:
                    current[kind] = line.strip()

        return units

    @property
    def default_photo(self):
        if not hasattr(self, '_photo'):
            self._photo = mktemp('.svg')
            with open(self._photo, 'w') as fhl:
                fhl.write("""
  
""")
            self.photos.append(self._photo)
        return self._photo

    def recieve_keys(self, *keys, **options):
        """Present the opotunity to add the key to the user:
         
        Returns
          - True if the key was already or is now imported.
          - False if keys were available but the user canceled.
          - None if no keys were found within the search.

        """
        keys = self.search_keys(*keys)
        if not keys:
            return None # User doesn't have GPG

        # Always use a temporary gpg home to review keys
        gpg = GPG(cmd=self.command, local=True) if not self.local else self

        # B. Import each of the keys
        gpg('--recv-keys', *zip(*keys)[0])

        # C. List keys (with photo options)
        choices = []
        for key in gpg.list_keys(photos=True):
            choices.append(key.get('photo', self.default_photo))
            choices.append('\n'.join(key['uid']))
            choices.append(key['key'])
            choices.append(str(key['expires']))

        if len(choices) / 4 == 1:
            title = "Can I use this GPG key to encrypt for this user?"
        else:
            title = "Please select the GPG key to use for encryption"

        # Show using gtk zenity (easier than gtk3 directly)
        p = Popen(['zenity',
            '--width', '900', '--height', '700', '--title', title,
            '--list', '--imagelist', '--print-column', '3',
              '--column', 'Photo ID',
              '--column', 'ID',
              '--column', 'Key',
              '--column', 'Expires',
            ] + choices, stdout=PIPE, stderr=PIPE)

        # Returncode is generated after communicate!
        key = p.communicate()[0].strip()

        # Select the default first key if one choice.
        # (person pressed ok without looking)
        if not key and len(choices) == 4:
            key = choices[2]

        if p.returncode != 0:
            # Cancel was pressed
            return False

        # E. Import the selected key
        self('--recv-keys', key)
        return self.status == 0

    def is_key_available(self, search):
        """Return False if the email is not found in the local key list"""
        self('--list-keys', search)
        if self.status == 2: # Keys not found
            return False
        # We return true, even if gpg returned some other kind of error
        # Because this prevents us running more commands to a broken gpg
        return True

    def search_keys(self, *keys):
        """Returns a list of (key_id, info) tuples from a search"""
        out = self('--search-keys', *keys)
        found = []
        prev = []
        for line in out.split("\n"):
            if line.startswith('gpg:'):
                continue
            if 'created:' in line:
                key_id = line.split('key ')[-1].split(',')[0]
                if '(revoked)' not in line:
                    found.append((key_id, prev))
                prev = []
            else:
                prev.append(line)
        return found


if __name__ == '__main__':
    cmd = sys.argv[0] + '.orig'
    if not os.path.isfile(cmd):
        sys.stderr.write("Can't find pass-through command '%s'\n" % args[0])
        sys.exit(-13)

    args = [cmd] + sys.argv[1:]
    # Check to see if call is from an application
    if 'GIO_LAUNCHED_DESKTOP_FILE' in os.environ:
        # We use our moved gpg command file
        gpg = GPG(cmd=cmd)
        # Check if we've got a missing key during an encryption, we get the
        # very next argument after a -r or -R argument (which should be
        # the email address)
        for recipient in [args[i+1] for (i, v) in enumerate(args) if v in ('-r', '-R')]:
            # Only check email addresses
            if '@' in recipient:
                if not gpg.is_key_available(recipient):
                    if gpg.recieve_keys(recipient) is None:
                        pass
                        # We can add a footer to the message here explaining GPG
                        # We can't do this, evolution will wrap it all up in a
                        # message structure.
                        #msg = sys.stdin.read()
                        #if msg:
                        #    msg += GPG_TRIED_FOOTER
                        #sys.stdout.write(msg)
                        #sys.exit(0)

    # We call and do not PIPE anything (pass-through)
    try:
        sys.exit(call(args))
    except KeyboardInterrupt:
        sys.exit(-14)

New Netboot Splash

I decided to update my netboot image, make it a lot more awesome looking when it comes on. So I decided to use an existing awesome background (no credit found) and add the required parts.

First this is the default netboot splash for 12.10, it hasn’t changed in a long time:

Next, this was the original netboot image I made three years ago:

Finally, this is my updated image:

Thoughts?

Stagered Rewards for Accomplishments

I do like the Accomplishments project, I think it could do great things when it gets to it’s 1.0 release.

Thinking about the design of the acc app, I was pondering why the trophies page always looks so cluttered and disorganised. One of the conclusions I’ve come to is that by rewarding every single action with a trophy, the trophy cabinet gets full very quickly and the very idea of a trophy looses it’s sparkle.

The design I would aim for to clean this up is a basic categorisation and substitution. when you’ve just accomplished your 100th ask ubuntu posting, you don’t really value a trophy for your first. Looking at Ask Ubuntu (and other reward based sites) I came to the conclusion that rewards should be staggered and that your score in any one category should be aggregated into a finale trophy.

With this representation of your accomplishments, it’s easy to see that you’re an awesome Ubuntu User, but you have lots of things you could do to get the full 3 star trophy for support. Each category of accomplishments can neatly fit in without creating more graphics for certain branding. That would only be required for the tasks/todo items page where you might want items to be dissociated with the idea that each task gets you a trophy.

What are your ideas?

Page Surfing: Improving Firefox in Ubuntu

I’m getting frustrated trying to scroll on pages and I’d like to introduce an idea:

Since we have a full screen app that really is taking up the entire screen, there seems to be a good opportunity to use the screen when an app is maximised to scroll the largest scrollbar in the app. I have no idea how hard it would be to tie the scroll bar into something which could be controlled via the operating system, technical details.

The idea here is to put the top and bottom infinities to use, allowing them to be used to allow easier viewing of the internet. This could obviously be made generic so it could be used to control all kinds of apps (optionally?).

Maybe this kind of thing could be made an extension, something to try out and do some experiments. Maybe others will see it as an essential part of their ubuntu experience.

Your thoughts, as always, welcome below…

Dictionary Icons

This week Michael Hall wrote a blog post about his foray into writing a new Dictionary Lens for Unity. It’s a lens that I would find most useful but until it’s packages I used the screenshots as a guide to how it would look.

I thought about how you would use the icon space to illustrate something useful about the words and definitions being shown. I thought that the type of the word is often not understood or remembered when looking up a word and often find myself reading over the abbr. italics. So I thought, how would you go about developing symbols for the concepts of word types?

In a fit of inkscape drafting, I put together a few concepts without colours and an open question to those interested; how would you iconify a concept?

Your comments below…

What are you Ubuntu, a Platform or a Product?

For today’s video blog I’m tackling the ideas behind Ubuntu the platform and Ubuntu the product, courtesy of Ayatana Mailing List. Nobody doesn’t like good Ayatana! Basically I dig into the problems between a One and Only vision and the more flexible, but harder to do, platform model of design.

With visual aids thanks to Inkscape!

Video Problems: Go directly to the video on blip.tv here and download the source ogv here.

What are your thoughts?

Whoa! Where’s it going?

After good healthy interest in yesterday’s video I decided to post the code in a repository (GPLv3 and CC-BY-SA) and as a second act to deliver Mark Shuttleworth’s feature request which I show off in the new video:

View Video on Blip

This is particularly cool since it means desktops will converge and look the same at certain dates as well as diverge and look different at all other times. What are your thoughts?

UDS: Design and Reduced Friction

I sent this to the Ayatana group and I’m posting here to my blog to invite any designers who are not in that group.

This is an invitation to all designers who will be at UDS-M soon to talk about reducing the resistance in the community to Ayatana developments and directions. So I want to kick off a discussion here and then carry
it on at UDS:

We’ve seen in the wider community resistance for a number of decisions that have been taken by the DX, Design and Ayatana groups in both Karmic and Lucid cycles. Various things seem to generate irritated users who are naturally not pleased about change.

As a community leader I don’t like this kind of fighting. So I’ve been thinking more about how to reduce the problems through better communication and conflict resolution.

I believe better communication doesn’t just mean talking in more places or going into more depth about the technical details of a design. It means using certain inviting language and designing the communication
for the audience, making sure that your course correcting each time there is a conflict of interest so the next communication includes as points what has been brought up before and doesn’t lead to duplication.

The observed culture has had a tendency to consider conflict as a bad thing, an all or nothing affair to disprove ever tenant. I’d like to encourage the view that conflicts are not about going all one way or all
another way but are about considering and factoring in the consideration into a variance so that the outcome is not exactly like either party predicted.

Some of that good dialectic goes on here in this mailing list, but that’s not communicated much outside where it would do good to calm people. I believe some of the problems stem from language of outside
publishings, e.g: “We’ve made this choice because we believe it’s better for normal users, there are no options and if your an advanced user, we’re not really thinking of you when we made this choice so please
don’t ask for us to add options.” (hyperbole, but you get the point)

Basically an invitation to get irate and not much of an invitation to come and help factor in various different positions and considerations. Not that this is the intention, I believe that the people writing these articles are really trying to communicate to the users. But language and ability to cope with user’s opinions seems to turn an opportunity to advance the design of Ubuntu into a flame war that ends up turning users off.

A few of my community circles react to Design Team news with a *sigh* and “Oh god what have they done now”. The teams reputation is low and it’s over shadowing the really great work that’s going on. How can I
convince people to trust decisions or even get involved if they don’t trust that the discussions are fair, balanced and considered? So I’d like to be able to build up social relations so that we’re not just on par with other teams, but surpass their ability to bring people in and form their world view into solid multi-consideration design.

I want people to think of Canonical and think of an awesome company that really get involved with it’s users (downstream) and it’s suppliers (upstream) and is really clever at blending everyone’s positions into something awesome. MPT is a perfect example, very good at considering and communicating effectively with the community. *cheer* Thanks! You’ve been great at calming various people.

Thoughts and Responses best put onto the Ayatana mailing list or held for UDS.