Reskinned Identity Selector per the JANET Wireframes documents.
authorDan Breslau <dbreslau@painless-security.com>
Thu, 25 Aug 2016 23:28:53 +0000 (19:28 -0400)
committerDan Breslau <dbreslau@painless-security.com>
Thu, 25 Aug 2016 23:28:53 +0000 (19:28 -0400)
Squashed commit of the following (details and minor updates trimmed;
all changes were by me.)

commit 2f8d4369433a277012679bf877633e686820eab8
    For consistency with GTK standards, put Cancel button on the left of the OK button

commit 42c3796bcbe056e7058652eda8751a853b6f745d
    Translatability tweaks

commit 872bb0c517c567c6b110a7ab26a47798b5b30555
    Fixed translatable strings

commit 5f52d1f4bbe0457ee8f625e297ea22b502a14e0f
    Added new source files with translatable strings

commit e2f8a250279d9a79a03d82995ef191d773627bfa
    Fixed regressions and new bugs in importing

commit 93475b1a2267539658150d8f8762edc391db93aa
    Support import in the Moonshot UI

commit abd4e8ca1595f56b61277704cd734722c46cfc44
    Cleaning up compilation warnings

commit 506cb264de13b4378445926925cb6e3b65bb0f58
    Improved spacing below IDCard name

commit 9439c0e393ac5e2bbfa5a6c59d1657d5254a5e1b
    Refactored to use a table for aligning widgets

commit 4f53091262cadc7233d8f27d32d48a4374fc85d2
    Search tooltip should really cover the whole search box, not just the icon

commit 0f63b63f6049760c1e81b1f091f7b51549be2935
    Use monospace font for fingerprint

commit 8cf33a3171f3a885b138e4f693acc996763ff7e6
    Removed remove_id_card(), which is no longer needed

commit 4001b9f4bf07db45f642c3ceab65bcd3c6279961
    Fixed selection failures when display was limited via search or selection.
    Selection is now managed by IdentityManagerView, and not by the CustomVBox.

    Removed "stop" icon from search box, and moved search icon to right side.

commit 1eeb1e05fcb7a5eb9e561815dd461d89d956bcba
    Fixed nai property; it no longer returns an unowned string. (This fixes a crash.)

commit b5719838ad36fb87c1dcd4469d73e4cb69124e7f
    Always print GLib errors to the log as well as stderr

commit abaf84cbf4dd9d7ce67605ff4e8bd51067bb0771
    Fixed display of the no_identity widget, and don't allow it to be removed

commit f2c571b08f033e29c7d69115d30ac368e81a3b72
    Added additional test IDs

commit 8b84367198d6b30c9c24ce81cfad2e9ec7f48ecb
    Fixed bugs in tracking TrustAnchor datetime-added

commit 3483be3d73af11f2bf7ce3318514c4fce50bebf4
    First pass at supporting date/time added for Trust Anchors

commit bdf3da196264fca4e79c010dfa0c7157d8f1b585
    Added libssl as a dependency

commit cdd1e39de9e29b801f366b5f7c29f7eeca09f8af
    Ugly hack to make the Yes button grab the default

commit 7ef2862128043325316a1494bfc23687fb30421f
    Intercept window close attempts, and ask user to confirm if selection is in progress

commit 7b7af6b7a02f69c6c6526a303b5532cb3d796f78
    Added right- or down-pointing arrow (for unselected and selected cards, respectively)

commit e0f0eed7f106405aa5570f98f3e0055132cf00ec
    Use the label text 'Do not use a Moonshot identity for this service' if displaying the ID Selector for ID selection

commit 507962f0c13e52d0d0d3669a6a8fbef191fb07c6
    Fixed line lengths for exporting PEM certificates.

    Also set default filename based on IdCard's display name,
    and go to the same directory by default if another cert is exported.

commit 55bd9a7d97e21699cc8b3dac780aa21f8edaece8
    Support exporting certificates in PEM format

commit 43e012afb2022bac4265c254dec9ab98cec993ba
    Decode the CA Certificate from binary, not from PEM format.

commit 9ae3e03e0c3088d8811af53dd9c3ea61ec79b670
    Fixed memory leaks

commit 50a68a489a85a8a57280a3a53d1ee9f589fb3487
    Implemented "Clear Trust Anchor" button in Edit Identity Dialog.

    Modified the "user verified" flag to be stored in the keystore (or local
    flat file), instead of in a Properties file.

    Modified the TrustAnchor class to be read-only, other than the "user_verified"
    field.

    Many logging updates.

commit b608c48ed9815e88c2f26e695016c3b4b266aca4
    Fixed compilation error and error handling.

commit 133e43a14fb57c62771186d4edb42fbab9e8fcf0
    Support decrypting a server CA certificate so we can get its expiration (valid-before) date.

commit 97500f7016809c2c6c168fff8d8f67f0243aa19d
    Don't show 'Trust anchor: None' label for new IdCards

commit 537f157a7a5fb7ea9e44a403a65621b81556b78a
    First pass at supporting trust anchors in IdCard dialog

commit 143c5ecc521ef29a781b1e15fa7372bcebc0c09d
    Updated About dialog; also bumped version number to 1.0.0

commit e32b9690270bbb4c4152ce9672db5d11a81f98e1
    First cut at supporting trust anchors

commit e9560299e59bf0845693d3b0c79b4deb4c7a9a68
    Fix appearance of services table

commit be7385061450cd85fcbcf9041c4c6295f59923aa
    Re-enable password hiding

commit 0572dfd787364f027ee41f3e1d9fa1293769cdca
    Refactored the IdCard services list to fix new bugs and (hopefully) prevent even newer ones.

commit 268826aff19670fb4a500b0724139b7cad6ed12f
    Support using libgee in moonshot-webp

commit 02de21ebf96bb1f20dfe380084dab92641cfa161
    Disallow editing the text fields of the No Identity card

commit a6135216493f33da3458e6befd221e41a1e3a054
    Changed IdCard.IsNoIdentity to IdCard.is_no_identity

commit 05d9a8b6c6e0dcf5aaa1c87e284c9f3c6a88ffa2
    Implemented 'Remember my identity choice for this service' checkbox on main selector dialog

commit 8693fa167c32a432187c5eedb24e5b16142c9774
    Implemented 'Do not show this message again' checkbox for warning dialogs

commit 509db435265cd41655adf4d4d31337381678b1f6
    Fixed (again) how the Add, Edit, Remove, and Send buttons are enabled/disabled

commit 1e7f448b4d54f4af3ed705ebaff5838262b25d4c
    Fixed (again) how the Add, Edit, Remove, and Send buttons are enabled/disabled

commit 53078345094f06a685445bcf67181c7e25736a76
    Updated the appearance of two warning dialogs.
    Also fixed (somewhat) the appearance of the menu bar in the main window.

commit 23366f80079adcd7b170e672de9957ccbf49f5a2
    Fixing AtkRelation and remember password checkbox issues

commit 6b328877c29b99d7b9a7ed73b4d65745179d1a7f
    Reskinning the password dialog

commit a971d079cb0acbd5f73377895c1c4418bd51179c
    Fix service prompt (again)

commit 08086626e9cacb32a9f1bf4ed3b16af198e368e0
    Fix prompting of ID for service

commit ffc9c2b032db74b2b4082eeee6db96e8e40944d3
    Set background to white, and fix sensitivity of Send button

commit 9e70ca46454122515d5af2daf10d7577732ed9b2
    Improved the appearance of the services table

commit 1c7207be9ee24b0bd786ab6739575b6fb8885194
    Field layout now conforms more closely to wireframes

commit 99f3b9007c4360666888f4fcdeaf6ccc82a595ba
    Dead code removal; also fixed Send button (at least a little; not tested yet.)

commit 1dad322094a84c630dde060aa2d1ac9c3b4dac1e
    Fix compilation with older versions of valac

commit ece5f632e3ee29f467feb52e5beb5650f5479add
    Add/Edit cards is now functionally complete (still needs aesthetic cleanup)

commit e7bac824b6c20465e609459205529c6f789bd888
    Renamed moonshot-add-dialog.vala to moonshot-identity-dialog.vala; refactoring to follow.

commit fe758926d68ee515d12f6e87e644514e3c4ab921
    Checkpointing: ID Selector (main dialog) now looks as specified by reskinning wireframes (more or less)

commit c13c41f51f8e5550d1f23d89f971bd3ba71c4c6f
    First pass at reskinning the main dialog (still need to update menu & detail views)

28 files changed:
.gitignore
Makefile.am
configure.ac
libmoonshot/libmoonshot-dbus.c
po/POTFILES.in
src/moonshot-add-dialog.vala [deleted file]
src/moonshot-crypto-utils.c [new file with mode: 0644]
src/moonshot-custom-vbox.vala
src/moonshot-id.vala
src/moonshot-idcard-store.vala
src/moonshot-idcard-widget.vala
src/moonshot-identities-manager.vala
src/moonshot-identity-dialog.vala [new file with mode: 0644]
src/moonshot-identity-management-view.vala
src/moonshot-identity-manager-app.vala
src/moonshot-identity-request.vala
src/moonshot-keyring-store.vala
src/moonshot-local-flat-file-store.vala
src/moonshot-logger.vala
src/moonshot-password-dialog.vala
src/moonshot-provisioning-common.vala
src/moonshot-server.vala
src/moonshot-settings.vala [new file with mode: 0644]
src/moonshot-trust-anchor-dialog.vala [new file with mode: 0644]
src/moonshot-utils.vala
src/moonshot-warning-dialog.vala [new file with mode: 0644]
src/moonshot-webp-parser.vala
webprovisioning/complex-test.msht

index 99486a0..b2b436a 100755 (executable)
@@ -34,6 +34,7 @@ src/moonshot-custom-vbox.c
 src/moonshot-id.c
 src/moonshot-idcard-widget.c
 src/moonshot-idcard-store.c
+src/moonshot-identity-dialog.c
 src/moonshot-identity-request.c
 src/moonshot-identity-management-view.c
 src/moonshot-identity-manager-app.c
@@ -45,7 +46,10 @@ src/moonshot-password-dialog.c
 src/moonshot-provisioning-common-new.vala
 src/moonshot-provisioning-common.c
 src/moonshot-server.c
+src/moonshot-settings.c
+src/moonshot-trust-anchor-dialog.c
 src/moonshot-utils.c
+src/moonshot-warning-dialog.c
 src/moonshot-webp-parser.c
 src/moonshot-window.c
 src/msrpc-client.c
index 7345b96..a578f76 100644 (file)
@@ -59,19 +59,24 @@ src_moonshot_SOURCES = \
         src/moonshot-keyring-store.vala \
         src/moonshot-idcard-store.vala \
         src/moonshot-id.vala \
-        src/moonshot-add-dialog.vala \
+        src/moonshot-identity-dialog.vala \
         src/moonshot-idcard-widget.vala \
         src/moonshot-custom-vbox.vala \
         src/moonshot-identities-manager.vala \
         src/moonshot-identity-request.vala \
         src/moonshot-server.vala \
+        src/moonshot-settings.vala \
         src/moonshot-password-dialog.vala \
         src/moonshot-provisioning-common.vala \
+        src/moonshot-trust-anchor-dialog.vala \
         src/moonshot-utils.vala \
         src/moonshot-futils.c \
-        src/moonshot-logger.vala
+        src/moonshot-crypto-utils.c \
+        src/moonshot-logger.vala \
+        src/moonshot-warning-dialog.vala
 
 src_moonshot_webp_SOURCES = \
+        src/moonshot-crypto-utils.c \
         src/moonshot-webp-parser.vala \
         src/moonshot-provisioning-common.vala \
         src/moonshot-id.vala \
@@ -83,7 +88,7 @@ src_moonshot_CPPFLAGS = $(moonshot_CFLAGS) $(AM_CPPFLAGS)
 src_moonshot_LDADD = $(moonshot_LIBS)
 src_moonshot_LDFLAGS = -g -O0 $(MOONSHOT_LOG_LIBS)
 
-src_moonshot_webp_VALAFLAGS = --vapidir=$(top_srcdir)/libmoonshot --pkg libmoonshot $(AM_VALAFLAGS)
+src_moonshot_webp_VALAFLAGS = --vapidir=$(top_srcdir)/libmoonshot  --pkg $(GEE_VERSION) --pkg libmoonshot $(AM_VALAFLAGS)
 src_moonshot_webp_CPPFLAGS = $(moonshot_CFLAGS) $(AM_CPPFLAGS)
 src_moonshot_webp_LDADD = $(moonshot_LIBS) ${top_builddir}/libmoonshot/libmoonshot.la
 src_moonshot_webp_LDFLAGS = $(MOONSHOT_LOG_LIBS)
@@ -118,8 +123,8 @@ if OS_LINUX
 
 AM_CPPFLAGS += -I/usr/include/gnome-keyring-1
 AM_VALAFLAGS += --pkg moonshot-gnome-keyring --define=GNOME_KEYRING
-src_moonshot_LDFLAGS += -lgnome-keyring
-src_moonshot_webp_LDFLAGS += -lgnome-keyring
+src_moonshot_LDFLAGS += -lgnome-keyring -lcrypto
+src_moonshot_webp_LDFLAGS += -lgnome-keyring -lcrypto
 
 ## Installing mime type data
 mimedir = $(datadir)/mime/packages
index 75b636a..6f15c9f 100644 (file)
@@ -1,6 +1,6 @@
 AC_PREREQ([2.63])
 AC_INIT([Moonshot-ui],
-        [0.7.2],
+        [1.0.0],
         [moonshot-community@jiscmail.ac.uk],
         [moonshot-ui],
         [http://www.project-moonshot.org/])
@@ -236,6 +236,7 @@ PKG_CHECK_MODULES(moonshot,[
         atk >= 1.20
         glib-2.0 >= 2.22
         gobject-2.0 >= 2.22
+        libssl
         $GTK_VERSION
         $GEE_VERSION
         $SERVER_IPC_MODULE
index 84f8975..e7c4b51 100644 (file)
@@ -435,6 +435,9 @@ int moonshot_install_id_card (const char     *display_name,
                        G_TYPE_INVALID);
 
     g_object_unref (dbus_proxy);
+    g_free(rules_patterns_strv);
+    g_free(rules_always_confirm_strv);
+    g_free(services_strv);
 
     if (g_error != NULL) {
         *error = moonshot_error_new (MOONSHOT_ERROR_IPC_ERROR,
index b330862..1e11dd8 100644 (file)
@@ -1,2 +1,11 @@
 # List of source files which contain translatable strings.
+src/moonshot-idcard-widget.vala
+src/moonshot-identities-manager.vala
+src/moonshot-identity-dialog.vala
 src/moonshot-identity-management-view.vala
+src/moonshot-identity-manager-app.vala
+src/moonshot-password-dialog.vala
+src/moonshot-server.vala
+src/moonshot-trust-anchor-dialog.vala
+src/moonshot-warning-dialog.vala
+src/moonshot-webp-parser.vala
diff --git a/src/moonshot-add-dialog.vala b/src/moonshot-add-dialog.vala
deleted file mode 100644 (file)
index dacf4f9..0000000
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- * Copyright (c) 2011-2014, JANET(UK)
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- *
- * 3. Neither the name of JANET(UK) nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
-*/
-using Gtk;
-
-
-// Defined here as workaround for emacs vala-mode indentation failure.
-#if VALA_0_12
-static const string CANCEL = Stock.CANCEL;
-#else
-static const string CANCEL = STOCK_CANCEL;
-#endif
-
-
-class AddIdentityDialog : Dialog
-{
-    static const string displayname_labeltext = _("Display Name");
-    static const string issuer_labeltext = _("Issuer");
-    static const string username_labeltext = _("Username");
-    static const string password_labeltext = _("Password");
-    private Entry displayname_entry;
-    private Label displayname_label;
-    private Entry issuer_entry;
-    private Label issuer_label;
-    private Entry username_entry;
-    private Label username_label;
-    private Entry password_entry;
-    private Label password_label;
-    private CheckButton remember_checkbutton;
-    private Label message_label;
-    public bool complete;
-    
-    public string display_name {
-        get { return displayname_entry.get_text(); }
-    }
-
-    public string issuer {
-        get { return issuer_entry.get_text(); }
-    }
-
-    public string username {
-        get { return username_entry.get_text(); }
-    }
-
-    public string password {
-        get { return password_entry.get_text(); }
-    }
-
-    public bool store_password {
-        get { return remember_checkbutton.active; }
-    }
-
-    public AddIdentityDialog()
-    {
-        this.set_title(_("Add ID Card"));
-        this.set_modal(true);
-
-        this.add_buttons(_("Add ID Card"), ResponseType.OK,
-                         CANCEL, ResponseType.CANCEL);
-        var content_area = this.get_content_area();
-        ((Box) content_area).set_spacing(12);
-        
-        displayname_label = new Label(@"$displayname_labeltext:");
-        displayname_label.set_alignment(1,(float) 0.5);
-        displayname_entry = new Entry();
-        issuer_label = new Label(@"$issuer_labeltext:");
-        issuer_label.set_alignment(1,(float) 0.5);
-        this.issuer_entry = new Entry();
-        username_label = new Label(@"$username_labeltext:");
-        username_label.set_alignment(1,(float) 0.5);
-        this.username_entry = new Entry();
-        password_label = new Label(@"$password_labeltext:");
-        password_label.set_alignment(1,(float) 0.5);
-        this.password_entry = new Entry();
-        password_entry.set_invisible_char('*');
-        password_entry.set_visibility(false);
-        this.remember_checkbutton = new CheckButton.with_label(_("Remember password"));
-        this.message_label = new Label("");
-        message_label.set_visible(false);
-
-        set_atk_relation(displayname_label, displayname_entry, Atk.RelationType.LABEL_FOR);
-        set_atk_relation(issuer_label, issuer_entry, Atk.RelationType.LABEL_FOR);
-        set_atk_relation(username_label, username_entry, Atk.RelationType.LABEL_FOR);
-        set_atk_relation(password_entry, password_entry, Atk.RelationType.LABEL_FOR);
-
-        var table = new Table(6, 2, false);
-        table.set_col_spacings(10);
-        table.set_row_spacings(10);
-        
-        table.attach_defaults(message_label, 0, 2, 0, 1);
-        table.attach_defaults(displayname_label, 0, 1, 1, 2);
-        table.attach_defaults(displayname_entry, 1, 2, 1, 2);
-        table.attach_defaults(issuer_label, 0, 1, 2, 3);
-        table.attach_defaults(issuer_entry, 1, 2, 2, 3);
-        table.attach_defaults(username_label, 0, 1, 3, 4);
-        table.attach_defaults(username_entry, 1, 2, 3, 4);
-        table.attach_defaults(password_label, 0, 1, 4, 5);
-        table.attach_defaults(password_entry, 1, 2, 4, 5);
-        table.attach_defaults(remember_checkbutton,  1, 2, 5, 6);
-
-        this.response.connect(on_response);
-        var vbox = new VBox(false, 0);
-        vbox.set_border_width(6);
-        vbox.pack_start(table, false, false, 0);
-
-        ((Container) content_area).add(vbox);
-
-        this.set_border_width(6);
-        this.set_resizable(false);
-        this.show_all();
-    }
-
-    private static string update_preamble(string preamble)
-    {
-        if (preamble == "")
-            return _("Missing required field: ");
-        return _("Missing required fields: ");
-    }
-
-    private static string update_message(string old_message, string new_item)
-    {
-        string message;
-        if (old_message == "")
-            message = new_item;
-        else
-            message = old_message + ", " + new_item;
-        return message;
-    }
-
-    private static void check_field(string field, Label label, string fieldname, ref string preamble, ref string message)
-    {
-        if (field != "") {
-            label.set_markup(@"$fieldname:");
-            return;
-        }
-        label.set_markup(@"<span foreground=\"red\">$fieldname:</span>");
-        preamble = update_preamble(preamble);
-        message = update_message(message, fieldname);
-    }
-
-    private bool check_fields()
-    {
-        string preamble = "";
-        string message = "";
-        string password_test = store_password ? password : "not required";
-        check_field(display_name, displayname_label, displayname_labeltext, ref preamble, ref message);
-        check_field(issuer, issuer_label, issuer_labeltext, ref preamble, ref message);
-        check_field(username, username_label, username_labeltext, ref preamble, ref message);
-        check_field(password_test, password_label, password_labeltext, ref preamble, ref message);
-        if (message != "") {
-            message_label.set_visible(true);
-            message_label.set_markup(@"<span foreground=\"red\">$preamble$message</span>");
-            return false;
-        }
-        return true;
-    }
-
-    private void on_response(Dialog source, int response_id)
-    {
-        switch (response_id) {
-        case ResponseType.OK:
-            complete = check_fields();
-            break;
-        case ResponseType.CANCEL:
-            complete = true;
-            break;
-        }
-    }
-
-    private void set_atk_relation(Widget widget, Widget target_widget, Atk.RelationType relationship)
-    {
-        var atk_widget = widget.get_accessible();
-        var atk_target_widget = target_widget.get_accessible();
-
-        atk_widget.add_relationship(relationship, atk_target_widget);
-    }
-}
diff --git a/src/moonshot-crypto-utils.c b/src/moonshot-crypto-utils.c
new file mode 100644 (file)
index 0000000..a14c610
--- /dev/null
@@ -0,0 +1,29 @@
+#include <string.h>
+#include <openssl/bio.h>
+#include <openssl/pem.h>
+
+#include <stdio.h>
+
+char* get_cert_valid_before(const unsigned char* buf, int len, char* datebuf, int datebuf_len)
+{
+    datebuf[0]='\0';
+
+    unsigned char *p = (unsigned char*) buf;
+    X509* x = d2i_X509(NULL, &p, len);
+    if (x == NULL) {
+        return "Error calling d2i_X509()!";
+    }
+
+    BIO* out_bio = BIO_new(BIO_s_mem());
+    ASN1_TIME* time = X509_get_notAfter(x);
+
+    if (ASN1_TIME_print(out_bio, time)) {
+        int write = BIO_read(out_bio, datebuf, datebuf_len - 1);
+        datebuf[write]='\0';
+    }
+
+    datebuf[datebuf_len - 1] = '\0';
+    BIO_free(out_bio);
+    X509_free(x);
+    return "";
+}
index 6d48e14..088f2a0 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -33,9 +33,10 @@ using Gtk;
 
 class CustomVBox : VBox
 {
-    public IdCardWidget current_idcard { get; set; default = null; }
+    static MoonshotLogger logger = get_logger("CustomVBox");
     private IdentityManagerView main_window; 
-
+    int next_pos = 0;
+    
     public CustomVBox(IdentityManagerView window, bool homogeneous, int spacing)
     {
         main_window = window;
@@ -43,7 +44,7 @@ class CustomVBox : VBox
         set_spacing(spacing);
     }
 
-    public void receive_expanded_event(IdCardWidget id_card_widget)
+    internal void receive_expanded_event(IdCardWidget id_card_widget)
     {
         var list = get_children();
         foreach (Widget id_card in list)
@@ -51,20 +52,41 @@ class CustomVBox : VBox
             if (id_card != id_card_widget)
                 ((IdCardWidget) id_card).collapse();
         }
-        current_idcard = id_card_widget;
         
-        if (current_idcard != null && main_window.request_queue.length > 0)
-            current_idcard.send_button.set_sensitive(true);
+        check_resize();
+    }
+
+    internal void receive_collapsed_event(IdCardWidget id_card_widget)
+    {
         check_resize();
     }
 
     public void add_id_card_widget(IdCardWidget id_card_widget)
     {
         pack_start(id_card_widget, false, false);
+        id_card_widget.position = next_pos++;
     }
 
-    public void remove_id_card_widget(IdCardWidget id_card_widget)
-    {
-        remove(id_card_widget);
+    public IdCardWidget? find_idcard_widget(IdCard id_card) {
+        foreach (var w in get_children()) {
+            IdCardWidget widget = (IdCardWidget) w;
+            if (widget.id_card.nai == id_card.nai) {
+                return widget;
+            }
+        }
+        return null;
     }
+
+    internal void clear()
+    {
+        logger.trace("clear");
+
+        var children = get_children();
+        foreach (var id_card_widget in children) {
+            remove(id_card_widget);
+        }
+
+        next_pos = 0;
+   }
+    
 }
index eae892f..b8e92a6 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
 */
+
+using Gee;
+
+extern char* get_cert_valid_before(uchar* inbuf, int inlen, char* outbuf, int outlen);
+
+
+// A TrustAnchor object can be imported or installed via the API, but cannot
+// be modified by the user, other than being cleared. Hence the fields are read-only.
 public class TrustAnchor : Object
 {
-    public string ca_cert {get; set; default = "";}
-    public string subject {get; set; default = "";}
-    public string subject_alt  {get; set; default = "";}
-    public string server_cert  {get; set; default = "";}
+    private static const string CERT_HEADER = "-----BEGIN CERTIFICATE-----";
+    private static const string CERT_FOOTER = "-----END CERTIFICATE-----";
+
+    public enum TrustAnchorType {
+        CA_CERT,
+        SERVER_CERT
+    }
+    private string _ca_cert = "";
+    private string _subject = "";
+    private string _subject_alt = "";
+    private string _server_cert = "";
+    private string _datetime_added = "";
+    public bool user_verified = false;
+
+    private static string fixup (string s) {
+        return (s == null ? "" : s.strip());
+    }
+
+    public TrustAnchor(string ca_cert, string server_cert, string subject, string subject_alt, bool user_verified) {
+        _ca_cert = fixup(ca_cert);
+        _server_cert = fixup(server_cert);
+        _subject = fixup(subject);
+        _subject_alt = fixup(subject_alt);
+        this.user_verified = user_verified;
+
+        // If we're reading from store, this will be overridden (see set_datetime_added)
+        _datetime_added = "";
+    }
+
+    public TrustAnchor.empty() {
+    }
+
+
+    public string ca_cert {
+        get {
+            return _ca_cert;
+        }
+    }
+
+    public string subject {
+        get {
+            return _subject;
+        }
+    }
+
+    public string subject_alt  {
+        get {
+            return _subject_alt;
+        }
+    }
+
+
+    public string server_cert {
+        get {
+            return _server_cert;
+        }
+    }
+
+    public string datetime_added {
+        get {
+            return _datetime_added;
+        }
+    }
+
+    public bool is_empty() {
+        return ca_cert == "" && subject == "" && subject_alt == "" && server_cert == "";
+    }
+
+    public TrustAnchorType get_anchor_type() {
+        return server_cert == "" ? TrustAnchorType.CA_CERT : TrustAnchorType.SERVER_CERT;
+    }
+
+    internal void set_datetime_added(string datetime) {
+        _datetime_added = fixup(datetime);
+    }
+
+    internal static string format_datetime_now() {
+        DateTime now = new DateTime.now_utc();
+        string dt = now.format("%b %d %T %Y %Z");
+        return dt;
+    }
+
     public int Compare(TrustAnchor other)
     {
-        if (this.ca_cert != other.ca_cert)
+        if (this.ca_cert != other.ca_cert) {
+            // IdCard.logger.trace("TrustAnchor.Compare: this.ca_cert='%s'; other.ca_cert='%s'".printf(this.ca_cert, other.ca_cert));
             return 1;
-        if (this.subject != other.subject)
+        }
+        if (this.subject != other.subject) {
+            // IdCard.logger.trace("TrustAnchor.Compare: this.subject='%s'; other.subject='%s'".printf(this.subject, other.subject));
             return 1;
-        if (this.subject_alt != other.subject_alt)
+        }
+        if (this.subject_alt != other.subject_alt) {
+            // IdCard.logger.trace("TrustAnchor.Compare: this.subject_alt='%s'; other.subject_alt='%s'".printf(this.subject_alt, other.subject_alt));
             return 1;
-        if (this.server_cert != other.server_cert)
+        }
+        if (this.server_cert != other.server_cert) {
+            // IdCard.logger.trace("TrustAnchor.Compare: this.server_cert=%s'; other.server_cert='%s'".printf(this.server_cert, other.server_cert));
             return 1;
+        }
+
+        // Do not compare the user_verified and datetime_added fields; they are not essential.
+
         return 0;
     }
+
+    public string? get_expiration_date(out string? err_out=null)
+    {
+        if (&err_out != null) {
+            err_out = null;
+        }
+
+        if (this.ca_cert == "") {
+            if (&err_out != null) {
+                err_out = "Trust anchor does not have a ca_certificate";
+                return null;
+            }
+        }
+
+        string cert = this.ca_cert;
+        cert.chomp();
+
+        uchar[] binary = Base64.decode(cert);
+        IdCard.logger.trace("get_expiration_date: encoded length=%d; decoded length=%d".printf(cert.length, binary.length));
+
+        char buf[64];
+        string err = (string) get_cert_valid_before(binary, binary.length, buf, 64);
+        if (err != "") {
+            IdCard.logger.error(@"get_expiration_date: get_cert_valid_before returned '$err'");
+            if (&err_out != null) {
+                err_out = err;
+            }
+            return null;
+        }
+            
+        string date = (string) buf;
+        IdCard.logger.trace(@"get_expiration_date: get_cert_valid_before returned '$date'");
+
+        return date;
+    }
 }
 
+
 public struct Rule
 {
     public string pattern;
@@ -64,13 +198,39 @@ public struct Rule
 
 public class IdCard : Object
 {
+    internal static MoonshotLogger logger = get_logger("IdCard");
+
     public const string NO_IDENTITY = "No Identity";
 
-    private string _nai;
-  
+    private string _username = "";
+    private string _issuer = "";
+
     public string display_name { get; set; default = ""; }
   
-    public string username { get; set; default = ""; }
+    public string username { 
+        public get {
+            return _username;
+        }
+        public set {
+            _username = value;
+            update_nai();
+        }
+    }
+
+    public string issuer { 
+        public get {
+            return _issuer;
+        }
+        public set {
+            _issuer = value;
+            update_nai();
+        }
+    }
+
+    private void update_nai() {
+        _nai = username + "@" + issuer;
+    }
+
 #if GNOME_KEYRING
     private unowned string _password;
     public string password {
@@ -90,29 +250,89 @@ public class IdCard : Object
     public string password { get; set; default = null; }
 #endif
 
-    public string issuer { get; set; default = ""; }
-  
     private Rule[] _rules = new Rule[0];
     public Rule[] rules {
         get {return _rules;}
         internal set {_rules = value ?? new Rule[0] ;}
     }
 
-    private string[] _services = new string[0];
-    public string[] services {
-        get {return _services;}
-        internal set {_services = value ?? new string[0] ;}
+    private ArrayList<string> _services = new ArrayList<string>();
+
+    internal ArrayList<string> services {
+         get {return  _services;}
+    }
+
+    // Returns the list of services as a string, using the given separator.
+    internal string get_services_string(string sep) {
+        if (_services.is_empty) {
+            return "";
+        }
+
+        // ArrayList.to_array() seems to be unreliable -- it causes segfaults 
+        // semi-randomly. (Possibly because it returns an unowned ref?)
+        // return string.joinv(sep, _services.to_array());
+        // 
+        // This problem may be related to the one noted elsewhere as the
+        // "Centos vala array property bug".
+
+        string[] svcs = new string[_services.size];
+        for (int i = 0; i < _services.size; i++) {
+            svcs[i] = _services[i];
+        }
+
+        return string.joinv(sep, svcs);
     }
 
+    internal void update_services(string[] services) {
+        _services.clear();
+
+        // Doesn't exist in older versions of libgee:
+        // _services.add_all_array(services);
+
+        if (services != null) {
+            foreach (string s in services) {
+                _services.add(s);
+            }
+        }
+    } 
+
+    internal void update_services_from_list(ArrayList<string> services) {
+        if (services == this._services) {
+            // Don't try to update from self.
+            return;
+        }
+
+        _services.clear();
+
+        if (services != null) {
+            _services.add_all(services);
+        }
+    } 
+
+
     public bool temporary {get; set; default = false; }
 
-    public TrustAnchor trust_anchor  { get; set; default = new TrustAnchor (); }
+    private TrustAnchor _trust_anchor = new TrustAnchor.empty();
+    public TrustAnchor trust_anchor  { 
+        get {
+            return _trust_anchor;
+        }
+    }
+
+    // For use by storage implementations.
+    internal void set_trust_anchor_from_store(TrustAnchor ta) {
+        _trust_anchor = ta;
+    }
+
+    internal void clear_trust_anchor() {
+        _trust_anchor = new TrustAnchor.empty();
+    }
   
-    public unowned string nai { get {  _nai = username + "@" + issuer; return _nai;}}
+    public string nai { public get; private set;}
 
     public bool store_password { get; set; default = false; }
 
-    public bool IsNoIdentity() 
+    public bool is_no_identity() 
     {
         return (display_name == NO_IDENTITY);
     }
@@ -145,13 +365,16 @@ public class IdCard : Object
         if (CompareRules(this.rules, other.rules)!=0)
             diff |= 1 << DiffFlags.RULES;
 
-        if (CompareStringArray(this.services, other.services)!=0)
+        if (CompareStringArrayList(this._services, other._services)!=0)
             diff |= 1 << DiffFlags.SERVICES;
 
         if (this.trust_anchor.Compare(other.trust_anchor)!=0)
             diff |= 1 << DiffFlags.TRUST_ANCHOR;
 
         // stdout.printf("Diff Flags: %x\n", diff);
+        if (this.display_name == other.display_name && diff != 0) {
+            logger.trace("Compare: Two IDs with display_name '%s', but diff_flags=%0x".printf(this.display_name, diff));
+        }
         return diff;
     }
 
@@ -169,10 +392,6 @@ public class IdCard : Object
     internal void add_rule(Rule rule) {
         _rules += rule;
     }
-
-    internal void add_service(string service) {
-        _services += service;
-    }
 }
 
 public int CompareRules(Rule[] a, Rule[] b)
@@ -189,13 +408,13 @@ public int CompareRules(Rule[] a, Rule[] b)
     return 0;
 }
 
-public int CompareStringArray(string[] a, string [] b)
+public int CompareStringArrayList(ArrayList<string> a, ArrayList<string> b)
 {
-    if (a.length != b.length) {
+    if (a.size != b.size) {
         return 1;
     }
 
-    for (int i = 0; i < a.length; i++) {
+    for (int i = 0; i < a.size; i++) {
         if (a[i] != b[i]) {
             return 1;
         }
index 3d6c863..b9c9368 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -42,5 +42,8 @@ public interface IIdentityCardStore : Object {
     public abstract IdCard? update_card(IdCard card);
     public abstract StoreType get_store_type();
     public abstract LinkedList<IdCard> get_card_list(); 
+
+    // Note that (at least right now) store_id_cards() will re-load the cards after saving them.
+    internal abstract void store_id_cards(); 
 }
 
index 6a9c8e8..03f4d61 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -33,106 +33,128 @@ using Gtk;
 
 class IdCardWidget : Box
 {
-    public IdCard id_card { get; set; default = null; }
+    // static MoonshotLogger logger = get_logger("IdCardWidget");
+
+    private static const ShadowType ARROW_SHADOW = ShadowType.NONE;
+
+    private IdentityManagerView manager_view;
 
+    public IdCard id_card { get; set; default = null; }
     private VBox main_vbox;
     private HBox table;
-    public Button delete_button { get; private set; default = null; }
-    public Button details_button { get; private set; default = null; }
-    public Button send_button { get; private set; default = null; }
-    private HButtonBox hbutton_box;
     private EventBox event_box;
+    private bool   is_selected = false;
+    private Arrow arrow;
     
     private Label label;
 
+    internal int _position = 0;
+    internal int position {
+        get {return _position;}
+        set {_position = value; set_idcard_color();}
+    }
+
     public signal void expanded();
-    public signal void remove_id();
-    public signal void details_id();
-    public signal void send_id();
+    public signal void collapsed();
 
-    public void collapse()
+    internal void select()
     {
-        this.hbutton_box.set_visible(false);
+        expand();
+        this.expanded();
+    }
 
-        set_idcard_color();
+    internal void unselect()
+    {
+        collapse();
+        this.collapsed();
     }
 
     public void expand()
     {
-        this.hbutton_box.set_visible(true);
+        is_selected = true;
+        update_id_card_label();
 
         set_idcard_color();
-        this.expanded();
+        arrow.set(ArrowType.DOWN, ARROW_SHADOW);
     }
 
-    private bool button_press_cb()
+    public void collapse()
     {
-        if (hbutton_box.get_visible())
-            collapse();
-        else
-            expand();
-
-        return false;
-    }
+        is_selected = false;
+        update_id_card_label();
 
-    private void delete_button_cb()
-    {
-        this.remove_id();
+        set_idcard_color();
+        arrow.set(ArrowType.RIGHT, ARROW_SHADOW);
     }
 
-    private void details_button_cb()
+    private bool button_press_cb()
     {
-        this.details_id();
-    }
+        if (is_selected)
+            unselect();
+        else
+            select();
 
-    private void send_button_cb()
-    {
-        this.send_id();
+        return false;
     }
 
     private void set_idcard_color()
     {
         var color = Gdk.Color();
 
-        if (hbutton_box.get_visible() == false)
+        if (is_selected)
         {
-            color.red = 65535;
-            color.green = 65535;
-            color.blue = 65535;
+                color.red = 0xd9 << 8;
+                color.green = 0xf7 << 8;
+                color.blue = 65535;
         }
-        else
-        {
-            color.red = 33333;
-            color.green = 33333;
-            color.blue = 60000;
+        else {
+            if (position % 2 == 0)
+            {
+                color.red = color.green = color.blue = 0xf2 << 8;
+            }
+            else
+            {
+                color.red = 65535;
+                color.green = 65535;
+                color.blue = 65535;
+
+            }
         }
-        var state = this.get_state();
-        this.event_box.modify_bg(state, color);
+        this.event_box.modify_bg(StateType.NORMAL, color);
+        this.arrow.modify_bg(StateType.NORMAL, color);
     }
     
-    public void
+    private void
     update_id_card_label()
     {
-        string services_text = "";
+        // !!TODO: Use a table to format the labels and values
+        string service_spacer = "\n                ";
 
-        var display_name = Markup.printf_escaped("<big>%s</big>", this.id_card.display_name);
-        for (int i = 0; i < id_card.services.length; i++)
+        var display_name = (manager_view.selection_in_progress() && this.id_card.is_no_identity()
+                            ? _("Do not use a Moonshot identity for this service") : this.id_card.display_name);
+        var label_text = Markup.printf_escaped("<span rise='8000'><big>%s</big></span>", display_name);
+
+        if (is_selected)
         {
-            var service = id_card.services[i];
-            
-            if (i == (id_card.services.length - 1))
-                services_text = services_text + Markup.printf_escaped("<i>%s</i>", service);
-            else
-                services_text = services_text + Markup.printf_escaped("<i>%s, </i>", service);
+            if (!this.id_card.is_no_identity()) {
+                label_text += "\n" + _("Username") + ":  " + id_card.username;
+                label_text += "\n" + _("Realm:") + "  " + id_card.issuer;
+                if (!id_card.trust_anchor.is_empty()) {
+                    label_text += "\n" + _("Trust anchor: Enterprise provisioned");
+                }
+            }
+
+            string services_text = _("Services:  ") + this.id_card.get_services_string(service_spacer);
+            label_text += "\n" + services_text;
         }
-        label.set_markup(display_name + "\n" + services_text);
+
+        label.set_markup(label_text);
     }
 
-    public IdCardWidget(IdCard id_card)
+    public IdCardWidget(IdCard id_card, IdentityManagerView manager_view)
     {
         this.id_card = id_card;
-
-        var image = new Image.from_pixbuf(get_pixbuf(id_card));
+        this.manager_view = manager_view;
 
         label = new Label(null);
         label.set_alignment((float) 0, (float) 0.5);
@@ -140,46 +162,29 @@ class IdCardWidget : Box
         update_id_card_label();
 
         table = new Gtk.HBox(false, 6);
+        var image = new Image.from_pixbuf(get_pixbuf(id_card));
+        if (this.id_card.is_no_identity()) {
+            image.clear();
+            // Use padding to make the image size =  48x48 (size = 2x padding)
+            image.set_padding(24, 24);
+        }
         table.pack_start(image, false, false, 0);
         table.pack_start(label, true, true, 0);
-
-        this.delete_button = new Button.with_label(_("Delete"));
-        this.details_button = new Button.with_label(_("View details"));
-        this.send_button = new Button.with_label(_("Send"));
-        set_atk_name_description(delete_button, _("Delete"), _("Delete this ID Card"));
-        set_atk_name_description(details_button, _("Details"), _("View the details of this ID Card"));
-        set_atk_name_description(send_button, _("Send"), _("Send this ID Card"));
-        this.hbutton_box = new HButtonBox();
-        hbutton_box.pack_end(delete_button);
-        hbutton_box.pack_end(details_button);
-        hbutton_box.pack_end(send_button);
-        send_button.set_sensitive(false);
-
-        delete_button.clicked.connect(delete_button_cb);
-        details_button.clicked.connect(details_button_cb);
-        send_button.clicked.connect(send_button_cb);
+        this.arrow = new Arrow(ArrowType.RIGHT, ARROW_SHADOW);
+        table.pack_start(arrow, false, false);
 
         this.main_vbox = new VBox(false, 12);
         main_vbox.pack_start(table, true, true, 0);
-        main_vbox.pack_start(hbutton_box, false, false, 0);
         main_vbox.set_border_width(12);
 
         event_box = new EventBox();
         event_box.add(main_vbox);
         event_box.button_press_event.connect(button_press_cb);
+        event_box.set_visible(false);
         this.pack_start(event_box, true, true);
 
         this.show_all();
-        this.hbutton_box.hide();
 
         set_idcard_color();
     }
-
-    private void set_atk_name_description(Widget widget, string name, string description)
-    {
-        var atk_widget = widget.get_accessible();
-
-        atk_widget.set_name(name);
-        atk_widget.set_description(description);
-    }
 }
index 182f2d6..01eea71 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -86,6 +86,8 @@ public class PasswordHashTable : Object {
 }
 
 public class IdentityManagerModel : Object {
+    static MoonshotLogger logger = get_logger("IdentityManagerModel");
+
     private const string FILE_NAME = "identities.txt";
     private PasswordHashTable password_table;
     private IIdentityCardStore store;
@@ -94,14 +96,14 @@ public class IdentityManagerModel : Object {
         identities.sort((a, b) => {
                 IdCard id_a = (IdCard )a;
                 IdCard id_b = (IdCard )b;
-                if (id_a.IsNoIdentity() && !id_b.IsNoIdentity()) {
+                if (id_a.is_no_identity() && !id_b.is_no_identity()) {
                     return -1;
-                } else if (id_b.IsNoIdentity() && !id_a.IsNoIdentity()) {
+                } else if (id_b.is_no_identity() && !id_a.is_no_identity()) {
                     return 1;
                 }
                 return strcmp(id_a.display_name, id_b.display_name);
             });
-        if (identities.is_empty || !identities[0].IsNoIdentity())
+        if (identities.is_empty || !identities[0].is_no_identity())
             identities.insert(0, IdCard.NewNoIdentity());
         foreach (IdCard id_card in identities) {
             if (!id_card.store_password) {
@@ -140,23 +142,32 @@ public class IdentityManagerModel : Object {
         return true;
     }
 
-    private bool remove_duplicates(IdCard card)
+    private bool remove_duplicates(IdCard new_card, out ArrayList<IdCard>? old_duplicates)
     {
-        bool duplicate_found = false;
-        bool found = false;
-        do {
-            var cards = this.store.get_card_list();
-            found = false;
-            foreach (IdCard id_card in cards) {
-                if ((card != id_card) && (id_card.nai == card.nai)) {
-                    stdout.printf("removing duplicate id for '%s'\n", card.nai);
-                    remove_card_internal(id_card);
-                    found = duplicate_found = true;
-                    break;
-                }
+        ArrayList<IdCard> dups = new ArrayList<IdCard>();
+        var cards = this.store.get_card_list();
+        foreach (IdCard id_card in cards) {
+            if ((new_card != id_card) && (id_card.nai == new_card.nai)) {
+                dups.add(id_card);
+            }
+        }
+
+        foreach (IdCard id_card in dups) {
+            logger.trace("removing duplicate id for '%s'\n".printf(new_card.nai));
+            remove_card_internal(id_card);
+
+            if (new_card.trust_anchor.Compare(id_card.trust_anchor) == 0) {
+                logger.trace("Old and new cards have same trust anchor. Re-using the datetime_added and user_verified fields from the old card.");
+                new_card.trust_anchor.set_datetime_added(id_card.trust_anchor.datetime_added);
+                new_card.trust_anchor.user_verified = id_card.trust_anchor.user_verified;
             }
-        } while (found);
-        return duplicate_found;
+        }
+
+        if (&old_duplicates != null) {
+            old_duplicates = dups;
+        }
+
+        return (dups.size > 0);
     }
 
     public IdCard? find_id_card(string nai, bool force_flat_file_store) {
@@ -178,9 +189,11 @@ public class IdentityManagerModel : Object {
         return retval;
     }
 
-    public void add_card(IdCard card, bool force_flat_file_store) {
-        if (card.temporary)
+    public void add_card(IdCard card, bool force_flat_file_store, out ArrayList<IdCard>? old_duplicates=null) {
+        if (card.temporary) {
+            logger.trace("add_card: card is temporary; returning.");
             return;
+        }
 
         string candidate;
         IIdentityCardStore.StoreType saved_store_type = get_store_type();
@@ -188,7 +201,7 @@ public class IdentityManagerModel : Object {
         if (force_flat_file_store)
             set_store_type(IIdentityCardStore.StoreType.FLAT_FILE);
 
-        remove_duplicates(card);
+        remove_duplicates(card, out old_duplicates);
 
         if (!display_name_is_valid(card.display_name, out candidate))
         {
@@ -197,12 +210,18 @@ public class IdentityManagerModel : Object {
 
         if (!card.store_password)
             password_table.CachePassword(card, store);
+
+        logger.trace("add_card: Adding card '%s' with services: '%s'"
+                     .printf(card.display_name, card.get_services_string("; ")));
+
         store.add_card(card);
         set_store_type(saved_store_type);
         card_list_changed();
     }
 
     public IdCard update_card(IdCard card) {
+        logger.trace("update_card");
+
         IdCard retval;
         if (card.temporary) {
             retval = card;
@@ -227,12 +246,16 @@ public class IdentityManagerModel : Object {
 
     public bool remove_card(IdCard card) {
         if (remove_card_internal(card)) {
+            logger.trace(@"remove_card: Removed '$(card.display_name)'");
             card_list_changed();
             return true;
         }
+        logger.warn(@"remove_card: Couldn't remove '$(card.display_name)'");
         return false;
     }
 
+    // The name is misleading: This not only sets the store type,
+    // it also creates a new store instance, which loads the card data.
     public void set_store_type(IIdentityCardStore.StoreType type) {
         if ((store != null) && (store.get_store_type() == type))
             return;
@@ -247,6 +270,21 @@ public class IdentityManagerModel : Object {
             store = new LocalFlatFileStore();
             break;
         }
+
+        // Loop through the loaded IDs. If any trust anchors are old enough that we didn't record
+        // the datetime_added, add it now.
+        string before_now = _("Before ") + TrustAnchor.format_datetime_now();
+        bool save_needed = false;
+        foreach (IdCard id in this.store.get_card_list()) {
+            if (!id.trust_anchor.is_empty() && id.trust_anchor.datetime_added == "") {
+                logger.trace("set_store_type : Set ta_datetime_added for old trust anchor on '%s' to '%s'".printf(id.display_name, before_now));
+                id.trust_anchor.set_datetime_added(before_now);
+                save_needed = true;
+            }
+        }
+        if (save_needed) {
+            this.store.store_id_cards();
+        }
     }
 
     public IIdentityCardStore.StoreType get_store_type() {
@@ -257,8 +295,8 @@ public class IdentityManagerModel : Object {
         foreach (IdCard card in this.store.get_card_list()) {
             // The 'NoIdentity' card is non-trivial if it has services or rules.
             // All other cards are automatically non-trivial.
-            if ((!card.IsNoIdentity()) || 
-                (card.services.length > 0) ||
+            if ((!card.is_no_identity()) || 
+                (card.services.size > 0) ||
                 (card.rules.length > 0)) {
                 return true;
             }
@@ -270,6 +308,7 @@ public class IdentityManagerModel : Object {
     private IdentityManagerApp parent;
 
     public IdentityManagerModel(IdentityManagerApp parent_app, IIdentityCardStore.StoreType store_type) {
+        logger.trace("IdentityManagerModel: store_type=" + store_type.to_string());
         parent = parent_app;
         password_table = new PasswordHashTable();
         set_store_type(store_type);
diff --git a/src/moonshot-identity-dialog.vala b/src/moonshot-identity-dialog.vala
new file mode 100644 (file)
index 0000000..3eb8c2b
--- /dev/null
@@ -0,0 +1,512 @@
+/*
+ * Copyright (c) 2016, JANET(UK)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of JANET(UK) nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+*/
+
+using Gee;
+using Gtk;
+
+
+// Defined here as workaround for emacs vala-mode indentation failure.
+#if VALA_0_12
+static const string CANCEL = Stock.CANCEL;
+#else
+static const string CANCEL = STOCK_CANCEL;
+#endif
+
+
+// For use when exporting certificates.
+static string export_directory = null;
+
+class IdentityDialog : Dialog
+{
+    private static Gdk.Color white = make_color(65535, 65535, 65535);
+    private static Gdk.Color selected_color = make_color(0xd9 << 8, 0xf7 << 8, 65535);
+
+    private static MoonshotLogger logger = get_logger("IdentityDialog");
+
+    static const string displayname_labeltext = _("Display Name");
+    static const string realm_labeltext = _("Realm");
+    static const string username_labeltext = _("Username");
+    static const string password_labeltext = _("Password");
+
+    private Entry displayname_entry;
+    private Label displayname_label;
+    private Entry realm_entry;
+    private Label realm_label;
+    private Entry username_entry;
+    private Label username_label;
+    private Entry password_entry;
+    private Label password_label;
+    private CheckButton remember_checkbutton;
+    private Label message_label;
+    public bool complete;
+    private IdCard card;
+
+    private Label selected_item = null;
+
+    // Whether to clear the card's TrustAnchor after the user selects OK
+    internal bool clear_trust_anchor = false;
+
+    public string display_name {
+        get { return displayname_entry.get_text(); }
+    }
+
+    public string issuer {
+        get { return realm_entry.get_text(); }
+    }
+
+    public string username {
+        get { return username_entry.get_text(); }
+    }
+
+    public string password {
+        get { return password_entry.get_text(); }
+    }
+
+    public bool store_password {
+        get { return remember_checkbutton.active; }
+    }
+
+    internal ArrayList<string> get_services()
+    {
+        return card.services;
+    }
+
+    public IdentityDialog(IdentityManagerView parent)
+    {
+        this.with_idcard(null, _("Add ID Card"), parent);
+    }
+
+    public IdentityDialog.with_idcard(IdCard? a_card, string title, IdentityManagerView parent)
+    {
+        bool is_new_card = false;
+        if (a_card == null)
+        {
+            is_new_card = true;
+        }
+
+        card = a_card ?? new IdCard();
+        this.set_title(title);
+        this.set_modal(true);
+        this.set_transient_for(parent);
+
+        this.add_buttons(CANCEL, ResponseType.CANCEL, _("OK"), ResponseType.OK);
+        Box content_area = (Box) this.get_content_area();
+
+        displayname_label = new Label(@"$displayname_labeltext:");
+        displayname_label.set_alignment(0, (float) 0.5);
+        displayname_entry = new Entry();
+        displayname_entry.set_text(card.display_name);
+        displayname_entry.set_width_chars(40);
+
+        realm_label = new Label(@"$realm_labeltext:");
+        realm_label.set_alignment(0, (float) 0.5);
+        realm_entry = new Entry();
+        realm_entry.set_text(card.issuer);
+        realm_entry.set_width_chars(60);
+
+        username_label = new Label(@"$username_labeltext:");
+        username_label.set_alignment(0, (float) 0.5);
+        username_entry = new Entry();
+        username_entry.set_text(card.username);
+        username_entry.set_width_chars(40);
+
+        password_label = new Label(@"$password_labeltext:");
+        password_label.set_alignment(0, (float) 0.5);
+
+        remember_checkbutton = new CheckButton.with_label(_("Remember password"));
+        remember_checkbutton.active = card.store_password;
+
+        password_entry = new Entry();
+        password_entry.set_invisible_char('*');
+        password_entry.set_visibility(false);
+        password_entry.set_width_chars(40);
+        password_entry.set_text(card.password);
+
+        message_label = new Label("");
+        message_label.set_visible(false);
+
+        set_atk_relation(displayname_label, displayname_entry, Atk.RelationType.LABEL_FOR);
+        set_atk_relation(realm_label, realm_entry, Atk.RelationType.LABEL_FOR);
+        set_atk_relation(username_label, username_entry, Atk.RelationType.LABEL_FOR);
+        set_atk_relation(password_label, password_entry, Atk.RelationType.LABEL_FOR);
+
+        content_area.pack_start(message_label, false, false, 6);
+        add_as_vbox(content_area, displayname_label, displayname_entry);
+        add_as_vbox(content_area, username_label, username_entry);
+        add_as_vbox(content_area, realm_label, realm_entry);
+        add_as_vbox(content_area, password_label, password_entry);
+
+        var remember_hbox = new HBox(false, 40);
+        remember_hbox.pack_start(new HBox(false, 0), false, false, 0);
+        remember_hbox.pack_start(remember_checkbutton, false, false, 0);
+        content_area.pack_start(remember_hbox, false, false, 2);
+
+        this.response.connect(on_response);
+        content_area.set_border_width(6);
+
+        if (!is_new_card)
+        {
+            Widget trust_anchor_box = make_trust_anchor_box(card);
+            content_area.pack_start(trust_anchor_box, false, false, 15);
+
+            var services_vbox = make_services_vbox();
+            content_area.pack_start(services_vbox);
+            var services_vbox_bottom_spacer = new Alignment(0, 0, 0, 0);
+            services_vbox_bottom_spacer.set_size_request(0, 12);
+            content_area.pack_start(services_vbox_bottom_spacer, false, false, 0);
+        }
+
+        if (card.is_no_identity())
+        {
+            displayname_entry.set_sensitive(false);
+            realm_entry.set_sensitive(false);
+            username_entry.set_sensitive(false);
+            password_entry.set_sensitive(false);
+            remember_checkbutton.set_sensitive(false);
+        }
+
+        this.set_border_width(6);
+        this.set_resizable(false);
+        this.modify_bg(StateType.NORMAL, white);
+        this.show_all();
+    }
+
+    private Widget make_trust_anchor_box(IdCard id)
+    {
+
+        Label ta_label = new Label(_("Trust anchor: ")
+                                   + (id.trust_anchor.is_empty() ? _("None") : _("Enterprise provisioned")));
+        ta_label.set_alignment(0, 0.5f);
+
+        if (id.trust_anchor.is_empty()) {
+            return ta_label;
+        }
+
+
+        AttachOptions fill_and_expand = AttachOptions.EXPAND | AttachOptions.FILL;
+        AttachOptions fill = AttachOptions.FILL;
+
+        Table ta_table = new Table(6, 2, false);
+        int row = 0;
+
+        var ta_clear_button = new Button.with_label(_("Clear Trust Anchor"));
+        ta_clear_button.clicked.connect((w) => {
+                clear_trust_anchor = true;
+                ta_table.set_sensitive(false);
+            }
+            );
+
+        ta_table.attach(ta_label, 0, 1, row, row + 1, fill_and_expand, fill_and_expand, 0, 0);
+        ta_table.attach(ta_clear_button, 1, 2, row, row + 1, fill, fill, 0, 0);
+        row++;
+
+        Label added_label = new Label(_("Added : " + id.trust_anchor.datetime_added));
+        added_label.set_alignment(0, 0.5f);
+        ta_table.attach(added_label, 0, 1, row, row + 1, fill_and_expand, fill_and_expand, 20, 5);
+        row++;
+
+        if (id.trust_anchor.get_anchor_type() == TrustAnchor.TrustAnchorType.SERVER_CERT) {
+            Widget fingerprint = make_ta_fingerprint_widget(id.trust_anchor);
+            ta_table.attach(fingerprint, 0, 1, row, row + 2, fill_and_expand, fill_and_expand, 5, 5);
+
+            // To make the fingerprint box wider, try:
+            // ta_table.attach(fingerprint, 0, 2, row, row + 2, fill_and_expand, fill_and_expand, 20, 5);
+
+        }
+        else {
+            Label ca_cert_label = new Label(_("CA Certificate:"));
+            ca_cert_label.set_alignment(0, 0.5f);
+            var export_button = new Button.with_label(_("Export Certificate"));
+            //!!TODO!
+            export_button.clicked.connect((w) => {export_certificate(id);});
+
+            ta_table.attach(ca_cert_label, 0, 1, row, row + 1, fill_and_expand, fill_and_expand, 20, 0);
+            ta_table.attach(export_button, 1, 2, row, row + 1, fill, fill, 0, 0);
+            row++;
+
+            //!!TODO: When to show Subject, and when (if ever) show Subject-Altname here?
+            Label subject_label = new Label(_("Subject: ") + id.trust_anchor.subject);
+            subject_label.set_alignment(0, 0.5f);
+            ta_table.attach(subject_label, 0, 1, row, row + 1, fill_and_expand, fill_and_expand, 40, 5);
+            row++;
+
+            Label expiration_label = new Label(_("Expiration date: ") + id.trust_anchor.get_expiration_date());
+            expiration_label.set_alignment(0, 0.5f);
+            ta_table.attach(expiration_label, 0, 1, row, row + 1, fill_and_expand, fill_and_expand, 40, 5);
+            row++;
+
+            //!!TODO: What *is* this?
+            Label constraint_label = new Label(_("Constraint: "));
+            constraint_label.set_alignment(0, 0.5f);
+            ta_table.attach(constraint_label, 0, 1, row, row + 1, fill_and_expand, fill_and_expand, 20, 0);
+            row++;
+        }
+
+        return ta_table;
+
+    }
+
+    private static void add_as_vbox(Box content_area, Label label, Entry entry)
+    {
+        VBox vbox = new VBox(false, 2);
+
+        vbox.pack_start(label, false, false, 0);
+        vbox.pack_start(entry, false, false, 0);
+
+        // Hack to prevent the text entries from stretching horizontally
+        HBox hbox = new HBox(false, 0);
+        hbox.pack_start(vbox, false, false, 0);
+        content_area.pack_start(hbox, false, false, 6);
+    }
+
+    private static string update_preamble(string preamble)
+    {
+        if (preamble == "")
+            return _("Missing required field: ");
+        return _("Missing required fields: ");
+    }
+
+    private static string update_message(string old_message, string new_item)
+    {
+        string message;
+        if (old_message == "")
+            message = new_item;
+        else
+            message = old_message + ", " + new_item;
+        return message;
+    }
+
+    private static void check_field(string field, Label label, string fieldname, ref string preamble, ref string message)
+    {
+        if (field != "") {
+            label.set_markup(@"$fieldname:");
+            return;
+        }
+        label.set_markup(@"<span foreground=\"red\">$fieldname:</span>");
+        preamble = update_preamble(preamble);
+        message = update_message(message, fieldname);
+    }
+
+    private bool check_fields()
+    {
+        string preamble = "";
+        string message = "";
+        string password_test = store_password ? password : "not required";
+        if (!card.is_no_identity())
+        {
+            check_field(display_name, displayname_label, displayname_labeltext, ref preamble, ref message);
+            check_field(username, username_label, username_labeltext, ref preamble, ref message);
+            check_field(issuer, realm_label, realm_labeltext, ref preamble, ref message);
+            check_field(password_test, password_label, password_labeltext, ref preamble, ref message);
+        }
+        if (message != "") {
+            message_label.set_visible(true);
+            message_label.set_markup(@"<span foreground=\"red\">$preamble$message</span>");
+            return false;
+        }
+        return true;
+    }
+
+    private void on_response(Dialog source, int response_id)
+    {
+        switch (response_id) {
+        case ResponseType.OK:
+            complete = check_fields();
+            break;
+        case ResponseType.CANCEL:
+            complete = true;
+            break;
+        }
+    }
+
+    private VBox make_services_vbox()
+    {
+        logger.trace("make_services_vbox");
+
+        var services_vbox_alignment = new Alignment(0, 0, 1, 0);
+        var services_vscroll = new ScrolledWindow(null, null);
+        services_vscroll.set_policy(PolicyType.NEVER, PolicyType.AUTOMATIC);
+        services_vscroll.set_shadow_type(ShadowType.IN);
+        services_vscroll.set_size_request(0, 60);
+        services_vscroll.add_with_viewport(services_vbox_alignment);
+
+#if VALA_0_12
+        var remove_button = new Button.from_stock(Stock.REMOVE);
+#else
+        var remove_button = new Button.from_stock(STOCK_REMOVE);
+#endif
+        remove_button.set_sensitive(false);
+
+
+        var services_table = new Table(card.services.size, 1, false);
+        services_table.set_row_spacings(1);
+        services_table.set_col_spacings(0);
+        services_table.modify_bg(StateType.NORMAL, white);
+
+        var table_button_hbox = new HBox(false, 6);
+        table_button_hbox.pack_start(services_vscroll, true, true, 4);
+
+        // Hack to prevent the button from growing vertically
+        VBox fixed_height = new VBox(false, 0);
+        fixed_height.pack_start(remove_button, false, false, 0);
+        table_button_hbox.pack_start(fixed_height, false, false, 0);
+
+        // A table doesn't have a background color, so put it in an EventBox, and
+        // set the EventBox's background color instead.
+        EventBox table_bg = new EventBox();
+        table_bg.modify_bg(StateType.NORMAL, white);
+        table_bg.add(services_table);
+        services_vbox_alignment.add(table_bg);
+
+        var services_vbox_title = new Label(_("Services:"));
+        services_vbox_title.set_alignment(0, 0.5f);
+
+        var services_vbox = new VBox(false, 6);
+        services_vbox.pack_start(services_vbox_title, false, false, 0);
+        services_vbox.pack_start(table_button_hbox, true, true, 0);
+
+        int i = 0;
+        foreach (string service in card.services)
+        {
+            var label = new Label(service);
+            label.set_alignment((float) 0, (float) 0);
+            label.xpad = 3;
+
+            EventBox event_box = new EventBox();
+            event_box.modify_bg(StateType.NORMAL, white);
+            event_box.add(label);
+            event_box.button_press_event.connect(() =>
+                {
+                    var state = label.get_state();
+                    logger.trace("button_press_callback: Label state=" + state.to_string() + " setting bg to " + white.to_string());
+
+                    if (selected_item == label)
+                    {
+                        // Deselect
+                        selected_item.parent.modify_bg(state, white);
+                        selected_item = null;
+                        remove_button.set_sensitive(false);
+                    }
+                    else
+                    {
+                        if (selected_item != null)
+                        {
+                            // Deselect
+                            selected_item.parent.modify_bg(state, white);
+                            selected_item = null;
+                        }
+
+                        // Select
+                        selected_item = label;
+                        selected_item.parent.modify_bg(state, selected_color);
+                        remove_button.set_sensitive(true);
+                    }
+                    return false;
+                });
+
+            services_table.attach_defaults(event_box, 0, 1, i, i+1);
+            i++;
+        }
+
+        remove_button.clicked.connect((remove_button) =>
+            {
+                var result = WarningDialog.confirm(this,
+                                                   Markup.printf_escaped(
+                                                       "<span font-weight='heavy'>You are about to remove the service '%s'.</span>",
+                                                       selected_item.label)
+                                                   + "\n\nAre you sure you want to do this?",
+                                                   "delete_service");
+
+                if (result)
+                {
+                    if (card != null) {
+                        card.services.remove(selected_item.label);
+                        services_table.remove(selected_item.parent);
+                        selected_item = null;
+                        remove_button.set_sensitive(false);
+                    }
+                }
+
+            });
+
+        return services_vbox;
+    }
+
+    private void export_certificate(IdCard id) 
+    {
+        var dialog = new FileChooserDialog("Save File",
+                                           this,
+                                           FileChooserAction.SAVE,
+                                           _("Cancel"),ResponseType.CANCEL,
+                                           _("Save"), ResponseType.ACCEPT,
+                                           null);
+        dialog.set_do_overwrite_confirmation(true);
+        if (export_directory != null) {
+            dialog.set_current_folder(export_directory);
+        }
+        // Remove slashes from the default filename.
+        string default_filename = 
+            (id.display_name + ".pem").replace(Path.DIR_SEPARATOR_S, "_");
+        dialog.set_current_name(default_filename);
+        if (dialog.run() == ResponseType.ACCEPT)
+        {
+            // Export the certificate in PEM format.
+
+            const string CERT_HEADER = "-----BEGIN CERTIFICATE-----\n";
+            const string CERT_FOOTER = "\n-----END CERTIFICATE-----\n";
+
+            // Strip any embedded newlines in the certificate...
+            string cert = id.trust_anchor.ca_cert.replace("\n", "");
+
+            // Re-embed newlines every 64 chars.
+            string newcert = CERT_HEADER;
+            while (cert.length > 63) {
+                newcert += cert[0:64];
+                newcert += "\n";
+                cert = cert[64:cert.length];
+            }
+            if (cert.length > 0) {
+                newcert += cert;
+            }
+            newcert += CERT_FOOTER;
+
+            string filename = dialog.get_filename();
+            var file  = File.new_for_path(filename);
+            var stream = file.replace(null, false, FileCreateFlags.PRIVATE);
+            stream.write(newcert.data);
+
+            // Save the parent directory to use as default for next save
+            export_directory = file.get_parent().get_path();
+        }
+        dialog.destroy();
+    }
+}
index 5ec2eab..55932f7 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
 */
 using Gee;
 using Gtk;
+using WebProvisioning;
 
 public class IdentityManagerView : Window {
     static MoonshotLogger logger = get_logger("IdentityManagerView");
 
-    private const int WINDOW_WIDTH = 400;
+    bool use_flat_file_store = false;
+
+    // The latest year in which Moonshot sources were modified.
+    private static int LATEST_EDIT_YEAR = 2016;
+
+    public static Gdk.Color white = make_color(65535, 65535, 65535);
+
+    private const int WINDOW_WIDTH = 700;
     private const int WINDOW_HEIGHT = 500;
     protected IdentityManagerApp parent_app;
     #if OS_MACOS
@@ -43,29 +51,26 @@ public class IdentityManagerView : Window {
     #endif
     private UIManager ui_manager = new UIManager();
     private Entry search_entry;
-    private VBox vbox_right;
-    private VBox login_vbox;
-    private VBox services_vbox;
     private CustomVBox custom_vbox;
-    private VBox services_internal_vbox;
-    private ScrolledWindow services_vscroll;
-    private Entry issuer_entry;
-    private Entry username_entry;
-    private Entry password_entry;
-    private Label prompting_service;
-    private Label no_identity_title;
-    private CheckButton remember_checkbutton;
-    private Button update_password_button;
+    private VBox service_prompt_vbox;
+    private Button edit_button;
+    private Button remove_button;
 
+    private Button send_button;
+    
     private Gtk.ListStore* listmodel;
     private TreeModelFilter filter;
 
-    public IdentityManagerModel identities_manager;
+    internal IdentityManagerModel identities_manager;
     private unowned SList<IdCard>    candidates;
 
-    public GLib.Queue<IdentityRequest> request_queue;
+    private GLib.Queue<IdentityRequest> request_queue;
 
-    private HashTable<Gtk.Button, string> service_button_map;
+    internal CheckButton remember_identity_binding = null;
+
+    private IdCard selected_idcard = null;
+
+    private string import_directory = null;
 
     private enum Columns
     {
@@ -77,28 +82,23 @@ public class IdentityManagerView : Window {
         N_COLUMNS
     }
 
-    private const string layout =
+    private const string menu_layout =
     "<menubar name='MenuBar'>" +
-    "        <menu name='FileMenu' action='FileMenuAction'>" +
-    "            <menuitem name='AddIdCard' action='AddIdCardAction' />" +
-    "            <separator />" +
-    "            <menuitem name='Quit' action='QuitAction' />" +
-    "        </menu>" +
-    "" +
     "        <menu name='HelpMenu' action='HelpMenuAction'>" +
     "             <menuitem name='About' action='AboutAction' />" +
     "        </menu>" +
     "</menubar>";
 
-    public IdentityManagerView(IdentityManagerApp app) {
+    public IdentityManagerView(IdentityManagerApp app, bool use_flat_file_store) {
         parent_app = app;
+        this.use_flat_file_store = use_flat_file_store;
+
         #if OS_MACOS
             osxApp = OSXApplication.get_instance();
         #endif
         identities_manager = parent_app.model;
         request_queue = new GLib.Queue<IdentityRequest>();
-        service_button_map = new HashTable<Gtk.Button, string>(direct_hash, direct_equal);
-        this.title = "Moonshot Identity Selector";
+        this.title = _("Moonshot Identity Selector");
         this.set_position(WindowPosition.CENTER);
         set_default_size(WINDOW_WIDTH, WINDOW_HEIGHT);
         build_ui();
@@ -107,7 +107,8 @@ public class IdentityManagerView : Window {
         connect_signals();
     }
     
-    public void on_card_list_changed() {
+    private void on_card_list_changed() {
+        logger.trace("on_card_list_changed");
         load_id_cards();
     }
     
@@ -163,7 +164,7 @@ public class IdentityManagerView : Window {
                     return true;
             }
             
-            if (id_card.services.length > 0)
+            if (id_card.services.size > 0)
             {
                 foreach (string service in id_card.services)
                 {
@@ -189,28 +190,10 @@ public class IdentityManagerView : Window {
         filter.set_visible_func(visible_func);
     }
 
-    private void search_entry_icon_press_cb(EntryIconPosition pos, Gdk.Event event)
-    {
-        if (pos == EntryIconPosition.PRIMARY)
-        {
-            print("Search entry icon pressed\n");
-        }
-        else
-        {
-            this.search_entry.set_text("");
-        }
-    }
-
     private void search_entry_text_changed_cb()
     {
         this.filter.refilter();
         redraw_id_card_widgets();
-
-        var has_text = this.search_entry.get_text_length() > 0;
-        this.search_entry.set_icon_sensitive(EntryIconPosition.PRIMARY, has_text);
-        this.search_entry.set_icon_sensitive(EntryIconPosition.SECONDARY, has_text);
-
-        this.vbox_right.set_visible(false);
     }
 
     private bool search_entry_key_press_event_cb(Gdk.EventKey e)
@@ -223,38 +206,10 @@ public class IdentityManagerView : Window {
         return false;
     }
 
-    private void update_password_cb()
-    {
-        if (this.custom_vbox.current_idcard != null) {
-            var identity = this.custom_vbox.current_idcard.id_card;
-            var dialog = new AddPasswordDialog(identity, null);
-            var result = dialog.run();
-
-            switch (result) {
-            case ResponseType.OK:
-                identity.password = dialog.password;
-                identity.store_password = dialog.remember;
-                if (dialog.remember)
-                    identity.temporary = false;
-                identity = identities_manager.update_card(identity);
-                break;
-            default:
-                break;
-            }
-            dialog.destroy();
-        }
-    }
-
     private void load_id_cards() {
-        string current_idcard_nai = null;
-        if (this.custom_vbox.current_idcard != null) {
-            current_idcard_nai = custom_vbox.current_idcard.id_card.nai;
-            custom_vbox.current_idcard = null;
-        }
-        var children = this.custom_vbox.get_children();
-        foreach (var id_card_widget in children) {
-            remove_id_card_widget((IdCardWidget)id_card_widget);
-        }   
+        logger.trace("load_id_cards");
+
+        custom_vbox.clear();
         this.listmodel->clear();
         LinkedList<IdCard> card_list = identities_manager.get_card_list() ;
         if (card_list == null) {
@@ -262,67 +217,25 @@ public class IdentityManagerView : Window {
         }
 
         foreach (IdCard id_card in card_list) {
+            logger.trace(@"load_id_cards: Loading card with display name '$(id_card.display_name)'");
             add_id_card_data(id_card);
-            IdCardWidget id_card_widget = add_id_card_widget(id_card);
-            if (id_card_widget.id_card.nai == current_idcard_nai) {
-                fill_details(id_card_widget);
-                id_card_widget.expand();
-            }
+            add_id_card_widget(id_card);
         }
-        if (custom_vbox.current_idcard == null)
-            fill_details(null);
     }
     
-    private void fill_details(IdCardWidget? id_card_widget)
-    {
-        logger.trace("fill_details: id_card_widget=%s".printf(id_card_widget == null ? "null" : "non-null"));
-
-        if (id_card_widget != null) {
-            var id_card = id_card_widget.id_card;
-            if (id_card.display_name == IdCard.NO_IDENTITY) {
-                logger.trace("fill_details: Displaying title for NO_IDENTITY");
-                login_vbox.hide();
-                no_identity_title.show_all();
-            } else {
-                logger.trace("fill_details: Displaying details for selected card");
-                this.issuer_entry.set_text(id_card.issuer);
-                this.username_entry.set_text(id_card.username);
-                this.password_entry.set_text(id_card.password ?? "");
-                this.remember_checkbutton.active = id_card.store_password;
-                no_identity_title.hide();
-                login_vbox.show_all();              
-            }
-
-            fill_services_vbox(id_card_widget.id_card);
-        }
-    }
-
-    private void show_details(IdCard id_card)
-    {
-        this.vbox_right.set_visible(!vbox_right.get_visible());
-
-        if (this.vbox_right.get_visible() == false)
-        {
-            this.resize(WINDOW_WIDTH, WINDOW_HEIGHT);
-        }
-    }
-
-    private void details_identity_cb(IdCardWidget id_card_widget)
-    {
-        fill_details(id_card_widget);
-        show_details(id_card_widget.id_card);
-    }
-
-    private IdCard get_id_card_data(AddIdentityDialog dialog)
+    private IdCard update_id_card_data(IdentityDialog dialog, IdCard id_card)
     {
-        var id_card = new IdCard();
-
         id_card.display_name = dialog.display_name;
         id_card.issuer = dialog.issuer;
         id_card.username = dialog.username;
         id_card.password = dialog.password;
         id_card.store_password = dialog.store_password;
-        id_card.services = {};
+
+        id_card.update_services_from_list(dialog.get_services());
+
+        if (dialog.clear_trust_anchor) {
+            id_card.clear_trust_anchor();
+        }
 
         return id_card;
     }
@@ -341,41 +254,52 @@ public class IdentityManagerView : Window {
                        Columns.PASSWORD_COL, id_card.password);
     }
 
-    private void remove_id_card_data(IdCard id_card)
+    private IdCardWidget add_id_card_widget(IdCard id_card)
     {
-        TreeIter iter;
-        string issuer;
+        logger.trace("add_id_card_widget: id_card.nai='%s'; selected nai='%s'"
+                     .printf(id_card.nai, 
+                             this.selected_idcard == null ? "[null selection]" : this.selected_idcard.nai));
 
-        if (listmodel->get_iter_first(out iter))
-        {
-            do
-            {
-                listmodel->get(iter,
-                               Columns.ISSUER_COL, out issuer);
 
-                if (id_card.issuer == issuer)
-                {
-                    listmodel->remove(iter);
-                    break;
-                }
-            }
-            while (listmodel->iter_next(ref iter));
+        var id_card_widget = new IdCardWidget(id_card, this);
+        this.custom_vbox.add_id_card_widget(id_card_widget);
+        id_card_widget.expanded.connect(this.widget_selected_cb);
+        id_card_widget.collapsed.connect(this.widget_unselected_cb);
+
+        if (this.selected_idcard != null && this.selected_idcard.nai == id_card.nai) {
+            logger.trace(@"add_id_card_widget: Expanding selected idcard widget");
+            id_card_widget.expand();
         }
+        return id_card_widget;
     }
 
-    private IdCardWidget add_id_card_widget(IdCard id_card)
+    private void widget_selected_cb(IdCardWidget id_card_widget)
     {
-        var id_card_widget = new IdCardWidget(id_card);
-        this.custom_vbox.add_id_card_widget(id_card_widget);
-        id_card_widget.details_id.connect(details_identity_cb);
-        id_card_widget.remove_id.connect(remove_identity_cb);
-        id_card_widget.send_id.connect((w) => send_identity_cb(w.id_card));
-        id_card_widget.expanded.connect(this.custom_vbox.receive_expanded_event);
-        id_card_widget.expanded.connect(fill_details);
-        return id_card_widget;
+        logger.trace(@"widget_selected_cb: id_card_widget.id_card.display_name='$(id_card_widget.id_card.display_name)'");
+
+        this.selected_idcard = id_card_widget.id_card;
+        bool allow_removes = !id_card_widget.id_card.is_no_identity();
+        this.remove_button.set_sensitive(allow_removes);
+        this.edit_button.set_sensitive(true);
+        this.custom_vbox.receive_expanded_event(id_card_widget);
+
+        if (this.selection_in_progress())
+             this.send_button.set_sensitive(true);
+    }
+
+    private void widget_unselected_cb(IdCardWidget id_card_widget)
+    {
+        logger.trace(@"widget_unselected_cb: id_card_widget.id_card.display_name='$(id_card_widget.id_card.display_name)'");
+
+        this.selected_idcard = null;
+        this.remove_button.set_sensitive(false);
+        this.edit_button.set_sensitive(false);
+        this.custom_vbox.receive_collapsed_event(id_card_widget);
+
+        this.send_button.set_sensitive(false);
     }
 
-    public bool add_identity(IdCard id_card, bool force_flat_file_store)
+    public bool add_identity(IdCard id_card, bool force_flat_file_store, out ArrayList<IdCard>? old_duplicates=null)
     {
         #if OS_MACOS
         /* 
@@ -386,9 +310,16 @@ public class IdentityManagerView : Window {
         #else
         Gtk.MessageDialog dialog;
         IdCard? prev_id = identities_manager.find_id_card(id_card.nai, force_flat_file_store);
+        logger.trace("add_identity(flat=%s, card='%s'): find_id_card returned %s"
+                     .printf(force_flat_file_store.to_string(), id_card.display_name, (prev_id != null ? prev_id.display_name : "null")));
         if (prev_id!=null) {
             int flags = prev_id.Compare(id_card);
+            logger.trace("add_identity: compare returned " + flags.to_string());
             if (flags == 0) {
+                if (&old_duplicates != null) {
+                    old_duplicates = new ArrayList<IdCard>();
+                }
+
                 return false; // no changes, no need to update
             } else if ((flags & (1 << IdCard.DiffFlags.DISPLAY_NAME)) != 0) {
                 dialog = new Gtk.MessageDialog(this,
@@ -421,22 +352,27 @@ public class IdentityManagerView : Window {
         #endif
 
         if (ret == Gtk.ResponseType.YES) {
-            this.identities_manager.add_card(id_card, force_flat_file_store);
+            this.identities_manager.add_card(id_card, force_flat_file_store, out old_duplicates);
             return true;
         }
-        return false;
+        else {
+            if (&old_duplicates != null) {
+                old_duplicates = new ArrayList<IdCard>();
+            }
+            return false;
+        }
     }
 
-    private void add_identity_manual_cb()
+    private void add_identity_cb()
     {
-        var dialog = new AddIdentityDialog();
+        var dialog = new IdentityDialog(this);
         int result = ResponseType.CANCEL;
         while (!dialog.complete)
             result = dialog.run();
 
         switch (result) {
         case ResponseType.OK:
-            this.identities_manager.add_card(get_id_card_data(dialog), false);
+            this.identities_manager.add_card(update_id_card_data(dialog, new IdCard()), false);
             break;
         default:
             break;
@@ -444,16 +380,37 @@ public class IdentityManagerView : Window {
         dialog.destroy();
     }
 
-    private void remove_id_card_widget(IdCardWidget id_card_widget) {
-        this.custom_vbox.remove_id_card_widget(id_card_widget);
+    private void edit_identity_cb(IdCard card)
+    {
+        var dialog = new IdentityDialog.with_idcard(card, _("Edit Identity"), this);
+        int result = ResponseType.CANCEL;
+        while (!dialog.complete)
+            result = dialog.run();
+
+        switch (result) {
+        case ResponseType.OK:
+            this.identities_manager.update_card(update_id_card_data(dialog, card));
+            break;
+        default:
+            break;
+        }
+        dialog.destroy();
     }
 
-    private void remove_identity(IdCardWidget id_card_widget)
+    private void remove_identity(IdCard id_card)
     {
-        var id_card = id_card_widget.id_card;
-        remove_id_card_widget(id_card_widget);
+        logger.trace(@"remove_identity: id_card.display_name='$(id_card.display_name)'");
+        if (id_card != this.selected_idcard) {
+            logger.error("remove_identity: id_card != this.selected_idcard!");
+        }
 
+        this.selected_idcard = null;
         this.identities_manager.remove_card(id_card);
+
+        // Nothing is selected, so disable buttons
+        this.edit_button.set_sensitive(false);
+        this.remove_button.set_sensitive(false);
+        this.send_button.set_sensitive(false);
     }
 
     private void redraw_id_card_widgets()
@@ -461,10 +418,7 @@ public class IdentityManagerView : Window {
         TreeIter iter;
         IdCard id_card;
 
-        var children = this.custom_vbox.get_children();
-        foreach (var id_card_widget in children) {
-            remove_id_card_widget((IdCardWidget )id_card_widget); //id_card_widget.destroy();
-        }
+        this.custom_vbox.clear();
 
         if (filter.get_iter_first(out iter))
         {
@@ -479,42 +433,67 @@ public class IdentityManagerView : Window {
         }
     }
 
-    private void remove_identity_cb(IdCardWidget id_card_widget)
+    private void remove_identity_cb(IdCard id_card)
     {
-        var id_card = id_card_widget.id_card;
-
-        var dialog = new MessageDialog(this,
-                                       DialogFlags.DESTROY_WITH_PARENT,
-                                       MessageType.QUESTION,
-                                       Gtk.ButtonsType.YES_NO,
-                                       _("Are you sure you want to delete %s ID Card?"), id_card.issuer);
-        var result = dialog.run();
-        switch (result) {
-        case ResponseType.YES:
-            remove_identity(id_card_widget);
-            break;
-        default:
-            break;
-        }
-        dialog.destroy();
+        bool remove = WarningDialog.confirm(this, 
+                                            Markup.printf_escaped(
+                                                "<span font-weight='heavy'>" + _("You are about to remove the identity '%s'.") + "</span>",
+                                                id_card.display_name)
+                                            + "\n\n" + _("Are you sure you want to do this?"),
+                                            "delete_idcard");
+        if (remove) 
+            remove_identity(id_card);
+    }
+
+    private void set_prompting_service(string service)
+    {
+        clear_selection_prompts();
+
+        var prompting_service = new Label(_("Identity requested for service:\n%s").printf(service));
+        prompting_service.set_line_wrap(true);
+
+        // left-align
+        prompting_service.set_alignment(0, (float )0.5);
+
+        var selection_prompt = new Label(_("Select your identity:"));
+        selection_prompt.set_alignment(0, 1);
+
+        this.service_prompt_vbox.pack_start(prompting_service, false, false, 12);
+        this.service_prompt_vbox.pack_start(selection_prompt, false, false, 2);
+        this.service_prompt_vbox.show_all();
     }
 
-    public void set_prompting_service(string service)
+    private void clear_selection_prompts()
     {
-        prompting_service.set_label( _("Identity requested for service: %s").printf(service) );
+        var list = service_prompt_vbox.get_children();
+        foreach (Widget w in list)
+        {
+            service_prompt_vbox.remove(w);
+        }
     }
 
+
     public void queue_identity_request(IdentityRequest request)
     {
-        if (this.request_queue.is_empty())
+        bool queue_was_empty = !this.selection_in_progress();
+        this.request_queue.push_tail(request);
+
+        if (queue_was_empty)
         { /* setup widgets */
             candidates = request.candidates;
             filter.refilter();
             redraw_id_card_widgets();
             set_prompting_service(request.service);
+            remember_identity_binding.show();
+
+            if (this.selected_idcard != null
+                && this.custom_vbox.find_idcard_widget(this.selected_idcard) != null) {
+                // A widget is already selected, and has not been filtered out of the display via search
+                send_button.set_sensitive(true);
+            }
+
             make_visible();
         }
-        this.request_queue.push_tail(request);
     }
 
 
@@ -534,10 +513,11 @@ public class IdentityManagerView : Window {
 
     public IdCard check_add_password(IdCard identity, IdentityRequest request, IdentityManagerModel model)
     {
+        logger.trace(@"check_add_password");
         IdCard retval = identity;
         bool idcard_has_pw = (identity.password != null) && (identity.password != "");
         bool request_has_pw = (request.password != null) && (request.password != "");
-        if ((!idcard_has_pw) && (!identity.IsNoIdentity())) {
+        if ((!idcard_has_pw) && (!identity.is_no_identity())) {
             if (request_has_pw) {
                 identity.password = request.password;
                 retval = model.update_card(identity);
@@ -563,18 +543,25 @@ public class IdentityManagerView : Window {
         return retval;
     }
 
-    public void send_identity_cb(IdCard id)
+    private void send_identity_cb(IdCard id)
     {
-        IdCard identity = id;
-        return_if_fail(request_queue.length > 0);
+        return_if_fail(this.selection_in_progress());
+
+        if (!check_and_confirm_trust_anchor(id)) {
+            // Allow user to pick again
+            return;
+        }
 
-        candidates = null;
         var request = this.request_queue.pop_head();
-        identity = check_add_password(identity, request, identities_manager);
-        if (this.request_queue.is_empty())
+        var identity = check_add_password(id, request, identities_manager);
+        send_button.set_sensitive(false);
+
+        candidates = null;
+      
+        if (!this.selection_in_progress())
         {
             candidates = null;
-            prompting_service.set_label(_(""));
+            clear_selection_prompts();
             if (!parent_app.explicitly_launched) {
 // The following occasionally causes the app to exit without sending the dbus
 // reply, so for now we just don't exit
@@ -590,119 +577,49 @@ public class IdentityManagerView : Window {
         filter.refilter();
         redraw_id_card_widgets();
 
-        if ((identity != null) && (!identity.IsNoIdentity()))
+        if ((identity != null) && (!identity.is_no_identity()))
             parent_app.default_id_card = identity;
 
-        request.return_identity(identity);
-    }
-
-    private void label_make_bold(Label label)
-    {
-        var font_desc = new Pango.FontDescription();
-
-        font_desc.set_weight(Pango.Weight.BOLD);
+        request.return_identity(identity, remember_identity_binding.active);
 
-        /* This will only affect the weight of the font, the rest is
-         * from the current state of the widget, which comes from the
-         * theme or user prefs, since the font desc only has the
-         * weight flag turned on.
-         */
-        label.modify_font(font_desc);
+        remember_identity_binding.active = false;
+        remember_identity_binding.hide();
     }
 
-    private void fill_services_vbox(IdCard id_card)
+    private bool check_and_confirm_trust_anchor(IdCard id)
     {
-        logger.trace("fill_services_vbox");
-
-        var children = this.services_internal_vbox.get_children();
-        foreach (var widget in children) {
-            services_internal_vbox.remove(widget);
-        }
-
-        int i = 0;
-        var n_rows = id_card.services.length;
-
-        var services_table = new Table(n_rows, 2, false);
-        services_table.set_col_spacings(10);
-        services_table.set_row_spacings(10);
-        this.services_internal_vbox.pack_start(services_table, true, false, 0);
-        
-        service_button_map.remove_all();
+        if (!id.trust_anchor.is_empty() && id.trust_anchor.get_anchor_type() == TrustAnchor.TrustAnchorType.SERVER_CERT) {
+            if (!id.trust_anchor.user_verified) {
 
-        foreach (string service in id_card.services)
-        {
-            var label = new Label(service);
-            label.set_alignment(0, (float) 0.5);
-#if VALA_0_12
-            var remove_button = new Button.from_stock(Stock.REMOVE);
-#else
-            var remove_button = new Button.from_stock(STOCK_REMOVE);
-#endif
+                bool ret = false;
+                int result = ResponseType.CANCEL;
+                var dialog = new TrustAnchorDialog(id, this);
+                while (!dialog.complete)
+                    result = dialog.run();
 
+                switch (result) {
+                case ResponseType.OK:
+                    id.trust_anchor.user_verified = true;
+                    ret = true;
+                    break;
+                default:
+                    break;
+                }
 
-            service_button_map.insert(remove_button, service);
-            
-            remove_button.clicked.connect((remove_button) =>
-                {
-                    var candidate = service_button_map.lookup(remove_button);
-                    if (candidate == null)
-                        return;
-                    var dialog = new Gtk.MessageDialog(this,
-                                                       Gtk.DialogFlags.DESTROY_WITH_PARENT,
-                                                       Gtk.MessageType.QUESTION,
-                                                       Gtk.ButtonsType.YES_NO,
-                                                       _("Are you sure you want to stop '%s' ID Card from being used with %s?"),
-                                                       custom_vbox.current_idcard.id_card.display_name,
-                                                       candidate);
-                    var ret = dialog.run();
-                    dialog.hide();
-              
-                    if (ret == Gtk.ResponseType.YES)
-                    {
-                        IdCard idcard = custom_vbox.current_idcard.id_card;
-                        if (idcard != null) {
-                            SList<string> services = new SList<string>();
-                
-                            foreach (string srv in idcard.services)
-                            {
-                                if (srv == candidate)
-                                    continue;
-                                services.append(srv);
-                            }
-                
-                            idcard.services = new string[services.length()];
-                            for (int j = 0; j < idcard.services.length; j++)
-                            {
-                                idcard.services[j] = services.nth_data(j);
-                            }
-                
-                            identities_manager.update_card(idcard);
-                        }
-                    }
-              
-                });
-            services_table.attach_defaults(label, 0, 1, i, i+1);
-            services_table.attach_defaults(remove_button, 1, 2, i, i+1);
-            i++;
+                dialog.destroy();
+                return ret;
+            }
         }
-
-        services_vbox.show_all();
+        return true;
     }
 
     private void on_about_action()
     {
-        string[] authors = {
-            "Javier Jardón <jjardon@codethink.co.uk>",
-            "Sam Thursfield <samthursfield@codethink.co.uk>",
-            "Alberto Ruiz <alberto.ruiz@codethink.co.uk>",
-            null
-        };
-
-        string copyright = "Copyright 2011 JANET";
+        string copyright = "Copyright (c) 2011, %d JANET".printf(LATEST_EDIT_YEAR);
 
         string license =
         """
-Copyright (c) 2011, JANET(UK)
+Copyright (c) 2011, %d JANET(UK)
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
@@ -731,52 +648,29 @@ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 SUCH DAMAGE.
-""";
-
-        Gtk.show_about_dialog(this,
-                              "comments", _("Moonshot project UI"),
-                              "copyright", copyright,
-                              "website", Config.PACKAGE_URL,
-                              "version", Config.PACKAGE_VERSION,
-                              "license", license,
-                              "website-label", _("Visit the Moonshot project web site"),
-                              "authors", authors,
-                              "translator-credits", _("translator-credits"),
-                              null
-            );
+""".printf(LATEST_EDIT_YEAR);
+
+        AboutDialog about = new AboutDialog();
+
+        about.set_comments(_("Moonshot project UI"));
+        about.set_copyright(copyright);
+        about.set_website(Config.PACKAGE_URL);
+        about.set_website_label(_("Visit the Moonshot project web site"));
+
+        // Note: The package version is configured at the top of moonshot/ui/configure.ac
+        about.set_version(Config.PACKAGE_VERSION);
+        about.set_license(license);
+        about.set_modal(true);
+        about.set_transient_for(this);
+        about.response.connect((a, b) => {about.destroy();});
+        about.modify_bg(StateType.NORMAL, white);
+        
+        about.run();
     }
 
     private Gtk.ActionEntry[] create_actions() {
         Gtk.ActionEntry[] actions = new Gtk.ActionEntry[0];
 
-        Gtk.ActionEntry filemenu = { "FileMenuAction",
-                                     null,
-                                     N_("_File"),
-                                     null, null, null };
-        actions += filemenu;
-        Gtk.ActionEntry add = { "AddIdCardAction",
-                                #if VALA_0_12
-                                Stock.ADD,
-                                #else
-                                STOCK_ADD,
-                                #endif
-                                N_("Add ID Card"),
-                                null,
-                                N_("Add a new ID Card"),
-                                add_identity_manual_cb };
-        actions += add;
-        Gtk.ActionEntry quit = { "QuitAction",
-                                 #if VALA_0_12
-                                 Stock.QUIT,
-                                 #else
-                                 STOCK_QUIT,
-                                 #endif
-                                 N_("Quit"),
-                                 "<control>Q",
-                                 N_("Quit the application"),
-                                 Gtk.main_quit };
-        actions += quit;
-
         Gtk.ActionEntry helpmenu = { "HelpMenuAction",
                                      null,
                                      N_("_Help"),
@@ -805,7 +699,7 @@ SUCH DAMAGE.
         ui_manager.insert_action_group(action_group, 0);
         try
         {
-            ui_manager.add_ui_from_string(layout, -1);
+            ui_manager.add_ui_from_string(menu_layout, -1);
         }
         catch (Error e)
         {
@@ -817,152 +711,139 @@ SUCH DAMAGE.
 
     private void build_ui()
     {
+        // Note: On Debian7/Gtk+2, the menu bar remains gray. This doesn't happen on Debian8/Gtk+3.
+        this.modify_bg(StateType.NORMAL, white);
+
         create_ui_manager();
 
+        int num_rows = 18;
+        int num_cols = 8;
+        int button_width = 1;
+
+        Table top_table = new Table(num_rows, 10, false);
+        top_table.set_border_width(12);
+
+        AttachOptions fill_and_expand = AttachOptions.EXPAND | AttachOptions.FILL;
+        AttachOptions fill = AttachOptions.FILL;
+        int row = 0;
+
+        service_prompt_vbox = new VBox(false, 0);
+        top_table.attach(service_prompt_vbox, 0, 1, row, row + 1, fill_and_expand, fill_and_expand, 12, 0);
+        row++;
+
+        string search_tooltip_text = _("Search for an identity or service");
         this.search_entry = new Entry();
 
         set_atk_name_description(search_entry, _("Search entry"), _("Search for a specific ID Card"));
-        this.search_entry.set_icon_from_pixbuf(EntryIconPosition.PRIMARY,
-                                               find_icon_sized("edit-find", Gtk.IconSize.MENU));
-//                                                find_icon_sized("edit-find-symbolic", Gtk.IconSize.MENU));
-        this.search_entry.set_icon_tooltip_text(EntryIconPosition.PRIMARY,
-                                                _("Search identity or service"));
-        this.search_entry.set_icon_sensitive(EntryIconPosition.PRIMARY, false);
-
         this.search_entry.set_icon_from_pixbuf(EntryIconPosition.SECONDARY,
-                                               find_icon_sized("process-stop", Gtk.IconSize.MENU));
-//                                                find_icon_sized("edit-clear-symbolic", Gtk.IconSize.MENU));
+                                               find_icon_sized("edit-find", Gtk.IconSize.MENU));
         this.search_entry.set_icon_tooltip_text(EntryIconPosition.SECONDARY,
-                                                _("Clear the current search"));
-        this.search_entry.set_icon_sensitive(EntryIconPosition.SECONDARY, false);
+                                                search_tooltip_text);
+
+        this.search_entry.set_tooltip_text(search_tooltip_text);
 
+        this.search_entry.set_icon_sensitive(EntryIconPosition.SECONDARY, false);
 
-        this.search_entry.icon_press.connect(search_entry_icon_press_cb);
         this.search_entry.notify["text"].connect(search_entry_text_changed_cb);
         this.search_entry.key_press_event.connect(search_entry_key_press_event_cb);
+        this.search_entry.set_width_chars(24);
 
-        this.custom_vbox = new CustomVBox(this, false, 6);
+        var search_label_markup ="<small>" + search_tooltip_text + "</small>";
+        var full_search_label = new Label(null);
+        full_search_label.set_markup(search_label_markup);
+        full_search_label.set_alignment(1, 0);
+
+        var search_vbox = new VBox(false, 0);
+        search_vbox.pack_start(search_entry, false, false, 0);
+        var search_spacer = new Alignment(0, 0, 0, 0);
+        search_spacer.set_size_request(0, 2);
+        search_vbox.pack_start(search_spacer, false, false, 0);
+        search_vbox.pack_start(full_search_label, false, false, 0);
+
+        // Overlap with the service_prompt_box
+        top_table.attach(search_vbox, 5, num_cols - button_width, row - 1, row + 1, fill_and_expand, fill, 0, 12);
+        row++;
+
+        this.custom_vbox = new CustomVBox(this, false, 2);
 
         var viewport = new Viewport(null, null);
-        viewport.set_border_width(6);
+        viewport.set_border_width(2);
         viewport.set_shadow_type(ShadowType.NONE);
         viewport.add(custom_vbox);
         var id_scrollwin = new ScrolledWindow(null, null);
         id_scrollwin.set_policy(PolicyType.NEVER, PolicyType.AUTOMATIC);
         id_scrollwin.set_shadow_type(ShadowType.IN);
         id_scrollwin.add_with_viewport(viewport);
-        this.prompting_service = new Label(_(""));
-        // left-align
-        prompting_service.set_alignment(0, (float )0.5);
-
-        var vbox_left = new VBox(false, 0);
-        vbox_left.pack_start(search_entry, false, false, 6);
-        vbox_left.pack_start(id_scrollwin, true, true, 0);
-        vbox_left.pack_start(prompting_service, false, false, 6);
-        vbox_left.set_size_request(WINDOW_WIDTH, 0);
-
-        this.no_identity_title = new Label(_("No Identity: Send this identity to services which should not use Moonshot"));
-        no_identity_title.set_alignment(0, (float ) 0.5);
-        no_identity_title.set_line_wrap(true);
-        no_identity_title.show();
-
-        var login_vbox_title = new Label(_("Login: "));
-        label_make_bold(login_vbox_title);
-        login_vbox_title.set_alignment(0, (float) 0.5);
-        var issuer_label = new Label(_("Issuer:"));
-        issuer_label.set_alignment(1, (float) 0.5);
-        this.issuer_entry = new Entry();
-        issuer_entry.set_can_focus(false);
-        var username_label = new Label(_("Username:"));
-        username_label.set_alignment(1, (float) 0.5);
-        this.username_entry = new Entry();
-        username_entry.set_can_focus(false);
-        var password_label = new Label(_("Password:"));
-        password_label.set_alignment(1, (float) 0.5);
-        this.password_entry = new Entry();
-        password_entry.set_invisible_char('*');
-        password_entry.set_visibility(false);
-        password_entry.set_sensitive(false);
-        this.remember_checkbutton = new CheckButton.with_label(_("Remember password"));
-        remember_checkbutton.set_sensitive(false);
-        this.update_password_button = new Button.with_label(_("Update Password"));
-        this.update_password_button.clicked.connect(update_password_cb);
-
-        set_atk_relation(issuer_label, issuer_entry, Atk.RelationType.LABEL_FOR);
-        set_atk_relation(username_label, username_entry, Atk.RelationType.LABEL_FOR);
-        set_atk_relation(password_entry, password_entry, Atk.RelationType.LABEL_FOR);
-
-        // Create the login_vbox. This starts off hidden, because the first card we
-        // display, by default, is NO_IDENTITY.
-        var login_table = new Table(5, 2, false);
-        login_table.set_col_spacings(10);
-        login_table.set_row_spacings(10);
-        login_table.attach_defaults(issuer_label, 0, 1, 0, 1);
-        login_table.attach_defaults(issuer_entry, 1, 2, 0, 1);
-        login_table.attach_defaults(username_label, 0, 1, 1, 2);
-        login_table.attach_defaults(username_entry, 1, 2, 1, 2);
-        login_table.attach_defaults(password_label, 0, 1, 2, 3);
-        login_table.attach_defaults(password_entry, 1, 2, 2, 3);
-        login_table.attach_defaults(remember_checkbutton,  1, 2, 3, 4);
-        login_table.attach_defaults(update_password_button, 0, 1, 4, 5);
-        var login_vbox_alignment = new Alignment(0, 0, 0, 0);
-        login_vbox_alignment.set_padding(0, 0, 12, 0);
-        login_vbox_alignment.add(login_table);
-        this.login_vbox = new VBox(false, 6);
-        login_vbox.pack_start(login_vbox_title, false, true, 0);
-        login_vbox.pack_start(login_vbox_alignment, false, true, 0);
-        login_vbox.hide();
-
-        var services_vbox_title = new Label(_("Services:"));
-        label_make_bold(services_vbox_title);
-        services_vbox_title.set_alignment(0, (float) 0.5);
-        
-        this.services_internal_vbox = new VBox(true, 6);
-
-        var services_vbox_alignment = new Alignment(0, 0, 0, 1);
-        services_vbox_alignment.set_padding(6, 6, 6, 6);
-        services_vbox_alignment.add(services_internal_vbox);
-        services_vscroll = new ScrolledWindow(null, null);
-        services_vscroll.set_policy(PolicyType.NEVER, PolicyType.AUTOMATIC);
-        services_vscroll.set_shadow_type(ShadowType.IN);
-        services_vscroll.add_with_viewport(services_vbox_alignment);
-
-        services_vbox = new VBox(false, 6);
-        this.vbox_right = new VBox(false, 18);
-        services_vbox.pack_start(services_vbox_title, false, false, 0);
-        services_vbox.pack_start(services_vscroll, true, true, 0);
-
-        vbox_right.pack_start(no_identity_title, true, false, 0);
-        vbox_right.pack_start(login_vbox, false, false, 0);
-        vbox_right.pack_start(services_vbox, true, true, 0);
-
-        var hbox = new HBox(false, 12);
-        hbox.pack_start(vbox_left, false, false, 0);
-        hbox.pack_start(vbox_right, true, true, 0);
+        top_table.attach(id_scrollwin, 0, num_cols - 1, row, num_rows - 1, fill_and_expand, fill_and_expand, 6, 0);
+
+        // Right below id_scrollwin:
+        remember_identity_binding = new CheckButton.with_label(_("Remember my identity choice for this service"));
+        remember_identity_binding.active = false;
+        top_table.attach(remember_identity_binding, 0, num_cols / 2, num_rows - 1, num_rows, fill_and_expand, fill_and_expand, 3, 0);
+
+        var add_button = new Button.with_label(_("Add"));
+        add_button.clicked.connect((w) => {add_identity_cb();});
+        top_table.attach(make_rigid(add_button), num_cols - button_width, num_cols, row, row + 1, fill, fill, 0, 0);
+        row++;
+
+        var import_button = new Button.with_label(_("Import"));
+        import_button.clicked.connect((w) => {import_identities_cb();});
+        top_table.attach(make_rigid(import_button), num_cols - button_width, num_cols, row, row + 1, fill, fill, 0, 0);
+        row++;
+
+        this.edit_button = new Button.with_label(_("Edit"));
+        edit_button.clicked.connect((w) => {edit_identity_cb(this.selected_idcard);});
+        edit_button.set_sensitive(false);
+        top_table.attach(make_rigid(edit_button), num_cols - button_width, num_cols, row, row + 1, fill, fill, 0, 0);
+        row++;
+
+        this.remove_button = new Button.with_label(_("Remove"));
+        remove_button.clicked.connect((w) => {remove_identity_cb(this.selected_idcard);});
+        remove_button.set_sensitive(false);
+        top_table.attach(make_rigid(remove_button), num_cols - button_width, num_cols, row, row + 1, fill, fill, 0, 0);
+        row++;
+
+        // push the send button down another row.
+        row++;
+        this.send_button = new Button.with_label(_("Send"));
+        send_button.clicked.connect((w) => {send_identity_cb(this.selected_idcard);});
+        // send_button.set_visible(false);
+        send_button.set_sensitive(false);
+        top_table.attach(make_rigid(send_button), num_cols - button_width, num_cols, row, row + 1, fill, fill, 0, 0);
+        row++;
 
         var main_vbox = new VBox(false, 0);
-        main_vbox.set_border_width(12);
 
-        #if OS_MACOS
+#if OS_MACOS
         // hide the  File | Quit menu item which is now on the Mac Menu
-        Gtk.Widget quit_item =  this.ui_manager.get_widget("/MenuBar/FileMenu/Quit");
-        quit_item.hide();
+//        Gtk.Widget quit_item =  this.ui_manager.get_widget("/MenuBar/FileMenu/Quit");
+//        quit_item.hide();
         
         Gtk.MenuShell menushell = this.ui_manager.get_widget("/MenuBar") as Gtk.MenuShell;
+        menushell.modify_bg(StateType.NORMAL, white);
+
         osxApp.set_menu_bar(menushell);
         osxApp.set_use_quartz_accelerators(true);
         osxApp.sync_menu_bar();
-        osxApp.ready(); 
+        osxApp.ready();
 #else
         var menubar = this.ui_manager.get_widget("/MenuBar");
         main_vbox.pack_start(menubar, false, false, 0);
+        menubar.modify_bg(StateType.NORMAL, white);
 #endif
-        main_vbox.pack_start(hbox, true, true, 0);
+        main_vbox.pack_start(top_table, true, true, 6);
+
         add(main_vbox);
         main_vbox.show_all();
-        this.vbox_right.hide();
+
+        if (!this.selection_in_progress())
+            remember_identity_binding.hide();
     } 
 
+    internal bool selection_in_progress() {
+        return !this.request_queue.is_empty();
+    }
+
     private void set_atk_name_description(Widget widget, string name, string description)
     {
         var atk_widget = widget.get_accessible();
@@ -973,15 +854,103 @@ SUCH DAMAGE.
 
     private void connect_signals()
     {
-        this.destroy.connect(Gtk.main_quit);
+        this.destroy.connect(() => {
+                logger.trace("Destroy event; calling Gtk.main_quit()");
+                Gtk.main_quit();
+            });
         this.identities_manager.card_list_changed.connect(this.on_card_list_changed);
+        this.delete_event.connect(() => {return confirm_quit();});
+    }
+
+    private bool confirm_quit() {
+        logger.trace("delete_event intercepted; selection_in_progress()=" + selection_in_progress().to_string());
+
+        if (selection_in_progress()) {
+            var result = WarningDialog.confirm(this,
+                                               Markup.printf_escaped(
+                                                   "<span font-weight='heavy'>" + _("Do you wish to use the %s service?") + "</span>",
+                                                   this.request_queue.peek_head().service)
+                                               + "\n\n" + _("Select Yes to select an ID for this service, or No to cancel"),
+                                               "close_moonshot_window");
+            if (result) {
+                // Prevent other handlers from handling this event; this keeps the window open.
+                return true; 
+            }
+        }
+
+        // Allow the window deletion to proceed.
+        return false;
     }
 
-    private static void set_atk_relation(Widget widget, Widget target_widget, Atk.RelationType relationship)
+    private static Widget make_rigid(Button button) 
     {
-        var atk_widget = widget.get_accessible();
-        var atk_target_widget = target_widget.get_accessible();
+        // Hack to prevent the button from growing vertically
+        VBox fixed_height = new VBox(false, 0);
+        fixed_height.pack_start(button, false, false, 0);
+
+        return fixed_height;
+    }
+
+    private void import_identities_cb() {
+        var dialog = new FileChooserDialog(_("Import File"),
+                                           this,
+                                           FileChooserAction.OPEN,
+                                           _("Cancel"),ResponseType.CANCEL,
+                                           _("Open"), ResponseType.ACCEPT,
+                                           null);
+
+        if (import_directory != null) {
+            dialog.set_current_folder(import_directory);
+        }
+
+        if (dialog.run() == ResponseType.ACCEPT)
+        {
+            // Save the parent directory to use as default for next save
+            string filename = dialog.get_filename();
+            var file  = File.new_for_path(filename);
+            import_directory = file.get_parent().get_path();
+
+            int import_count = 0;
+
+            var webp = new Parser(filename);
+            dialog.destroy();
+            webp.parse();
+            logger.trace(@"import_identities_cb: Have $(webp.cards.length) IdCards");
+            foreach (IdCard card in webp.cards)
+            {
+
+                if (card == null) {
+                    logger.trace(@"import_identities_cb: Skipping null IdCard");
+                    continue;
+                }
 
-        atk_widget.add_relationship(relationship, atk_target_widget);
+                if (!card.trust_anchor.is_empty()) {
+                    string ta_datetime_added = TrustAnchor.format_datetime_now();
+                    card.trust_anchor.set_datetime_added(ta_datetime_added);
+                    logger.trace("import_identities_cb : Set ta_datetime_added for '%s' to '%s'; ca_cert='%s'; server_cert='%s'"
+                                 .printf(card.display_name, ta_datetime_added, card.trust_anchor.ca_cert, card.trust_anchor.server_cert));
+                }
+
+
+                bool result = add_identity(card, use_flat_file_store);
+                if (result) {
+                    logger.trace(@"import_identities_cb: Added or updated '$(card.display_name)'");
+                    import_count++;
+                }
+                else {
+                    logger.trace(@"import_identities_cb: Did not add or update '$(card.display_name)'");
+                }
+            }
+            var msg_dialog = new Gtk.MessageDialog(this,
+                                               Gtk.DialogFlags.DESTROY_WITH_PARENT,
+                                               Gtk.MessageType.INFO,
+                                               Gtk.ButtonsType.OK,
+                                               _("Import completed. %d Identities were added or updated."),
+                                               import_count);
+            msg_dialog.run();
+            msg_dialog.destroy();
+        }
+        dialog.destroy();
     }
+
 }
index 9a5e59c..0170139 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -69,10 +69,6 @@ public class IdentityManagerApp {
     }
 #endif
 
-    private const int WINDOW_WIDTH = 400;
-    private const int WINDOW_HEIGHT = 500;
-
-
     /** If we're successfully registered with DBus, then show the UI. Otherwise, wait until we're registered. */
     public void show() {
         if (name_is_owned) {
@@ -112,7 +108,7 @@ public class IdentityManagerApp {
             model.set_store_type(IIdentityCardStore.StoreType.KEYRING);
 
         if (!headless)
-            view = new IdentityManagerView(this);
+            view = new IdentityManagerView(this, use_flat_file_store);
         LinkedList<IdCard> card_list = model.get_card_list();
         if (card_list.size > 0)
             this.default_id_card = card_list.last();
@@ -128,14 +124,21 @@ public class IdentityManagerApp {
 #endif
     }
 
-    public bool add_identity(IdCard id, bool force_flat_file_store) {
-        if (view != null) return view.add_identity(id, force_flat_file_store);
-        model.add_card(id, force_flat_file_store);
-        return true;
+    public bool add_identity(IdCard id, bool force_flat_file_store, out ArrayList<IdCard>? old_duplicates=null) {
+        if (view != null) 
+        {
+            logger.trace("add_identity: calling view.add_identity");
+            return view.add_identity(id, force_flat_file_store, out old_duplicates);
+        }
+        else {
+            logger.trace("add_identity: calling model.add_card");
+            model.add_card(id, force_flat_file_store, out old_duplicates);
+            return true;
+        }
     }
 
     public void select_identity(IdentityRequest request) {
-        logger.trace("select_identity");
+        logger.trace("select_identity: request.nai=%s".printf(request.nai ?? "[null]"));
 
         IdCard identity = null;
 
@@ -155,6 +158,7 @@ public class IdentityManagerApp {
                 /* If NAI matches, use this id card */
                 if (has_nai && request.nai == id.nai)
                 {
+                    logger.trace("select_identity: request has nai; returning " + id.display_name);
                     identity = id;
                     break;
                 }
@@ -162,13 +166,9 @@ public class IdentityManagerApp {
                 /* If any service matches we add id card to the candidate list */
                 if (has_srv)
                 {
-                    foreach (string srv in id.services)
-                    {
-                        if (request.service == srv)
-                        {
-                            request.candidates.append(id);
-                            continue;
-                        }
+                    if (id.services.contains(request.service)) {
+                        logger.trace(@"select_identity: request has service '$(request.service); matched on '$(id.display_name)'");
+                        request.candidates.append(id);
                     }
                 }
             }
@@ -176,45 +176,17 @@ public class IdentityManagerApp {
             /* If more than one candidate we dissasociate service from all ids */
             if ((identity == null) && has_srv && request.candidates.length() > 1)
             {
+                logger.trace(@"select_identity: multiple candidates; removing service '$(request.service) from all.");
                 foreach (IdCard id in request.candidates)
                 {
-                    int i = 0;
-                    SList<string> services_list = null;
-                    bool has_service = false;
-
-                    foreach (string srv in id.services)
-                    {
-                        if (srv == request.service)
-                        {
-                            has_service = true;
-                            continue;
-                        }
-                        services_list.append(srv);
-                    }
-                    
-                    if (!has_service)
-                        continue;
-
-                    if (services_list.length() == 0)
-                    {
-                        id.services = {};
-                        continue;
-                    }
-
-                    string[] services = new string[services_list.length()];
-                    foreach (string srv in services_list)
-                    {
-                        services[i] = srv;
-                        i++;
-                    }
-
-                    id.services = services;
+                    id.services.remove(request.service);
                 }
             }
 
             /* If there are no candidates we use the service matching rules */
             if ((identity == null) && (request.candidates.length() == 0))
             {
+                logger.trace("select_identity: No candidates; using service matching rules.");
                 foreach (IdCard id in model.get_card_list())
                 {
                     foreach (Rule rule in id.rules)
@@ -222,6 +194,7 @@ public class IdentityManagerApp {
                         if (!match_service_pattern(request.service, rule.pattern))
                             continue;
 
+                        logger.trace(@"select_identity: ID $(id.display_name) matched on service matching rules.");
                         request.candidates.append(id);
 
                         if (rule.always_confirm == "true")
@@ -231,6 +204,7 @@ public class IdentityManagerApp {
             }
             
             if ((identity == null) && has_nai) {
+                logger.trace("select_identity: Creating temp identity");
                 // create a temp identity
                 string[] components = request.nai.split("@", 2);
                 identity = new IdCard();
@@ -318,7 +292,7 @@ public class IdentityManagerApp {
                 if (!shown) {
                     GLib.error("Couldn't own name org.janet.Moonshot on dbus or show previously launched identity manager.");
                 } else {
-                    stdout.printf("Showed previously launched identity manager.\n");
+                    stdout.printf(_("Showed previously launched identity manager.\n"));
                     GLib.Process.exit(0);
                 }
             }
@@ -482,6 +456,7 @@ public static int main(string[] args) {
     settings.set_long_property("gtk-menu-images", 0, "moonshot");
 #endif
 
+    //TODO?? Do we need to call Intl.setlocale(LocaleCategory.MESSAGES, "");
     Intl.bindtextdomain(Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
     Intl.bind_textdomain_codeset(Config.GETTEXT_PACKAGE, "UTF-8");
     Intl.textdomain(Config.GETTEXT_PACKAGE);
index b00a5e5..4068903 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -32,6 +32,8 @@
 public delegate void ReturnIdentityCallback(IdentityRequest request);
 
 public class IdentityRequest : Object {
+    static MoonshotLogger logger = get_logger("IdentityRequest");
+
     public IdCard? id_card = null;
     public bool complete = false;
     public bool select_default = false;
@@ -79,35 +81,27 @@ public class IdentityRequest : Object {
         return false;
     }
 
-    public void return_identity(IdCard? id_card) {
+    public void return_identity(IdCard? id_card, bool update_card = true) {
         this.id_card = id_card;
         this.complete = true;
 
         /* update id_card service list */
-        if (id_card != null && this.service != null && this.service != "")
+        if (update_card && id_card != null && this.service != null && this.service != "")
         {
-            bool duplicate_service = false;
-
-            foreach (string service in id_card.services)
-            {
-                if (service == this.service)
-                    duplicate_service = true;
-            }
+            bool duplicate_service = id_card.services.contains(this.service);
+            logger.trace("return_identity: duplicate_service=" + duplicate_service.to_string());
             if (duplicate_service == false)
             {
-                string[] services = new string[id_card.services.length + 1];
-
-                for (int i = 0; i < id_card.services.length; i++)
-                    services[i] = id_card.services[i];
-
-                services[id_card.services.length] = this.service;
-                id_card.services = services;
+                logger.trace("return_identity: calling add_service");
+                id_card.services.add(this.service);
+                logger.trace("return_identity: back from add_service");
 
                 this.id_card = this.parent_app.model.update_card(id_card);
             }
         }
 
         return_if_fail(callback != null);
+        logger.trace("return_identity: invoking callback");
         callback(this);
     }
 
index 6b36022..8ca5e31 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -33,25 +33,35 @@ using Gee;
 
 #if GNOME_KEYRING
 public class KeyringStore : Object, IIdentityCardStore {
+    static MoonshotLogger logger = get_logger("KeyringStore");
+
     private LinkedList<IdCard> id_card_list;
     private const string keyring_store_attribute = "Moonshot";
     private const string keyring_store_version = "1.0";
     private const GnomeKeyring.ItemType item_type = GnomeKeyring.ItemType.GENERIC_SECRET;
 
     public void add_card(IdCard card) {
+        logger.trace("add_card: Adding card '%s' with services: '%s'"
+                     .printf(card.display_name, card.get_services_string("; ")));
+
         id_card_list.add(card);
         store_id_cards();
     }
 
     public IdCard? update_card(IdCard card) {
+        logger.trace("update_card");
+
         id_card_list.remove(card);
         id_card_list.add(card);
+
         store_id_cards();
         foreach (IdCard idcard in id_card_list) {
             if (idcard.display_name == card.display_name) {
                 return idcard;
             }
         }
+
+        logger.error(@"update_card: card '$(card.display_name)' was not found after re-loading!");
         return null;
     }
 
@@ -97,9 +107,19 @@ public class KeyringStore : Object, IIdentityCardStore {
             int rules_patterns_index = -1;
             int rules_always_confirm_index = -1;
             string store_password = null;
+            string ca_cert = "";
+            string server_cert = "";
+            string subject = "";
+            string subject_alt = "";
+            bool   user_verified = false;
+            string ta_datetime_added = "";
             for (i = 0; i < entry.attributes.len; i++) {
                 var attribute = ((GnomeKeyring.Attribute *) entry.attributes.data)[i];
-                string value = attribute.string_value;
+                string value = "";
+                if (attribute.type == GnomeKeyring.AttributeType.STRING) {
+                    value = attribute.string_value;
+                }
+
                 if (attribute.name == "Issuer") {
                     id_card.issuer = value;
                 } else if (attribute.name == "Username") {
@@ -107,23 +127,34 @@ public class KeyringStore : Object, IIdentityCardStore {
                 } else if (attribute.name == "DisplayName") {
                     id_card.display_name = value;
                 } else if (attribute.name == "Services") {
-                    id_card.services = value.split(";");
+                    id_card.update_services(value.split(";"));
                 } else if (attribute.name == "Rules-Pattern") {
                     rules_patterns_index = i;
                 } else if (attribute.name == "Rules-AlwaysConfirm") {
                     rules_always_confirm_index = i;
                 } else if (attribute.name == "CA-Cert") {
-                    id_card.trust_anchor.ca_cert = value.strip();
+                    ca_cert = value.strip();
                 } else if (attribute.name == "Server-Cert") {
-                    id_card.trust_anchor.server_cert = value;
+                    server_cert = value;
                 } else if (attribute.name == "Subject") {
-                    id_card.trust_anchor.subject = value;
+                    subject = value;
                 } else if (attribute.name == "Subject-Alt") {
-                    id_card.trust_anchor.subject_alt = value;
+                    subject_alt = value;
                 } else if (attribute.name == "StorePassword") {
                     store_password = value;
+                } else if (attribute.name == "TA_User_Verified") {
+                    user_verified = (value == "true");
+                } else if (attribute.name == "TA_DateTime_Added") {
+                    ta_datetime_added = value;
                 }
             }
+
+            var ta = new TrustAnchor(ca_cert, server_cert, subject, subject_alt, user_verified);
+            if (ta_datetime_added != "") {
+                ta.set_datetime_added(ta_datetime_added);
+            }
+            id_card.set_trust_anchor_from_store(ta);
+
             if ((rules_always_confirm_index != -1) && (rules_patterns_index != -1)) {
                 string rules_patterns_all = ((GnomeKeyring.Attribute *) entry.attributes.data)[rules_patterns_index].string_value;
                 string rules_always_confirm_all = ((GnomeKeyring.Attribute *) entry.attributes.data)[rules_always_confirm_index].string_value;
@@ -152,12 +183,12 @@ public class KeyringStore : Object, IIdentityCardStore {
         }
     }
 
-    public void store_id_cards() {
+    internal void store_id_cards() {
+        logger.trace("store_id_cards");
         clear_keyring();
         foreach (IdCard id_card in this.id_card_list) {
             /* workaround for Centos vala array property bug: use temp array */
             var rules = id_card.rules;
-            var services_array = id_card.services;
             string[] rules_patterns = new string[rules.length];
             string[] rules_always_conf = new string[rules.length];
             
@@ -167,7 +198,7 @@ public class KeyringStore : Object, IIdentityCardStore {
             }
             string patterns = string.joinv(";", rules_patterns);
             string always_conf = string.joinv(";", rules_always_conf);
-            string services = string.joinv(";", services_array);
+            string services = id_card.get_services_string(";");
             GnomeKeyring.AttributeList attributes = new GnomeKeyring.AttributeList();
             uint32 item_id;
             attributes.append_string(keyring_store_attribute, keyring_store_version);
@@ -181,6 +212,8 @@ public class KeyringStore : Object, IIdentityCardStore {
             attributes.append_string("Server-Cert", id_card.trust_anchor.server_cert);
             attributes.append_string("Subject", id_card.trust_anchor.subject);
             attributes.append_string("Subject-Alt", id_card.trust_anchor.subject_alt);
+            attributes.append_string("TA_User_Verified", id_card.trust_anchor.user_verified ? "true" : "false");
+            attributes.append_string("TA_DateTime_Added", id_card.trust_anchor.datetime_added);
             attributes.append_string("StorePassword", id_card.store_password ? "yes" : "no");
 
             GnomeKeyring.Result result = GnomeKeyring.item_create_sync(null,
index 8281d9d..8cc905c 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -32,6 +32,8 @@
 using Gee; 
 
 public class LocalFlatFileStore : Object, IIdentityCardStore {
+    static MoonshotLogger logger = get_logger("LocalFlatFileStore");
+
     private LinkedList<IdCard> id_card_list;
     private const string FILE_NAME = "identities.txt";
 
@@ -44,9 +46,12 @@ public class LocalFlatFileStore : Object, IIdentityCardStore {
         id_card_list.remove(card);
         id_card_list.add(card);
         store_id_cards();
-        foreach(IdCard idcard in id_card_list)
-        if (idcard.display_name == card.display_name)
-            return idcard;
+        foreach(IdCard idcard in id_card_list) {
+            if (idcard.display_name == card.display_name) {
+                return idcard;
+            }
+        }
+        logger.error(@"update_card: card '$(card.display_name)' was not found after re-loading!");
         return null;
     }
 
@@ -71,6 +76,7 @@ public class LocalFlatFileStore : Object, IIdentityCardStore {
         var key_file = new KeyFile();
         var path = get_data_dir();
         var filename = Path.build_filename(path, FILE_NAME);
+        logger.trace("load_id_cards: attempting to load from " + filename);
         
         try {
             key_file.load_from_file(filename, KeyFileFlags.NONE);
@@ -88,7 +94,7 @@ public class LocalFlatFileStore : Object, IIdentityCardStore {
                 id_card.issuer = key_file.get_string(identity, "Issuer");
                 id_card.username = key_file.get_string(identity, "Username");
                 id_card.password = key_file.get_string(identity, "Password");
-                id_card.services = key_file.get_string_list(identity, "Services");
+                id_card.update_services(key_file.get_string_list(identity, "Services"));
                 id_card.display_name = key_file.get_string(identity, "DisplayName");
                 if (key_file.has_key(identity, "StorePassword")) {
                     id_card.store_password = (key_file.get_string(identity, "StorePassword") == "yes");
@@ -111,14 +117,21 @@ public class LocalFlatFileStore : Object, IIdentityCardStore {
                 }
                 
                 // Trust anchor 
-                id_card.trust_anchor.ca_cert = key_file.get_string(identity, "CA-Cert").strip();
-                id_card.trust_anchor.subject = key_file.get_string(identity, "Subject");
-                id_card.trust_anchor.subject_alt = key_file.get_string(identity, "SubjectAlt");
-                id_card.trust_anchor.server_cert = key_file.get_string(identity, "ServerCert");
-
+                string ca_cert = key_file.get_string(identity, "CA-Cert").strip();
+                string server_cert = key_file.get_string(identity, "ServerCert");
+                string subject = key_file.get_string(identity, "Subject");
+                string subject_alt = key_file.get_string(identity, "SubjectAlt");
+                bool  user_verified = get_bool_setting(identity, "TA_User_Verified", false, key_file);
+                var ta = new TrustAnchor(ca_cert, server_cert, subject, subject_alt, user_verified);
+                string ta_datetime_added = get_string_setting(identity, "TA_DateTime_Added", "", key_file);
+                if (ta_datetime_added != "") {
+                    ta.set_datetime_added(ta_datetime_added);
+                }
+                id_card.set_trust_anchor_from_store(ta);
                 id_card_list.add(id_card);
             }
             catch (Error e) {
+                logger.error("load_id_cards: Error while loading keyfile: %s\n".printf(e.message));
                 stdout.printf("Error:  %s\n", e.message);
             }
         }
@@ -135,13 +148,13 @@ public class LocalFlatFileStore : Object, IIdentityCardStore {
         return path;
     }
     
-    public void store_id_cards() {
+    internal void store_id_cards() {
         var key_file = new KeyFile();
         foreach (IdCard id_card in this.id_card_list) {
+            logger.trace(@"store_id_cards: Storing '$(id_card.display_name)'");
+
             /* workaround for Centos vala array property bug: use temp arrays */
             var rules = id_card.rules;
-            var services = id_card.services;
-            string[] empty = {};
             string[] rules_patterns = new string[rules.length];
             string[] rules_always_conf = new string[rules.length];
             
@@ -157,7 +170,15 @@ public class LocalFlatFileStore : Object, IIdentityCardStore {
                 key_file.set_string(id_card.display_name, "Password", id_card.password);
             else
                 key_file.set_string(id_card.display_name, "Password", "");
-            key_file.set_string_list(id_card.display_name, "Services", services ?? empty);
+
+            // Using id_card.services.to_array() seems to cause a crash, possibly due to
+            // an unowned reference to the array.
+            string[] svcs = new string[id_card.services.size];
+            for (int i = 0; i < id_card.services.size; i++) {
+                svcs[i] = id_card.services[i];
+            }
+
+            key_file.set_string_list(id_card.display_name, "Services", svcs);
 
             if (rules.length > 0) {
                 key_file.set_string_list(id_card.display_name, "Rules-Patterns", rules_patterns);
@@ -166,10 +187,15 @@ public class LocalFlatFileStore : Object, IIdentityCardStore {
             key_file.set_string(id_card.display_name, "StorePassword", id_card.store_password ? "yes" : "no");
             
             // Trust anchor 
-            key_file.set_string(id_card.display_name, "CA-Cert", id_card.trust_anchor.ca_cert ?? "");
-            key_file.set_string(id_card.display_name, "Subject", id_card.trust_anchor.subject ?? "");
-            key_file.set_string(id_card.display_name, "SubjectAlt", id_card.trust_anchor.subject_alt ?? "");
-            key_file.set_string(id_card.display_name, "ServerCert", id_card.trust_anchor.server_cert ?? "");
+            key_file.set_string(id_card.display_name, "CA-Cert", id_card.trust_anchor.ca_cert);
+            key_file.set_string(id_card.display_name, "Subject", id_card.trust_anchor.subject);
+            key_file.set_string(id_card.display_name, "SubjectAlt", id_card.trust_anchor.subject_alt);
+            key_file.set_string(id_card.display_name, "ServerCert", id_card.trust_anchor.server_cert);
+            if (id_card.trust_anchor.datetime_added != "") {
+                key_file.set_string(id_card.display_name, "TA_DateTime_Added", id_card.trust_anchor.datetime_added);
+            }
+            key_file.set_boolean(id_card.display_name, "TA_User_Verified", id_card.trust_anchor.user_verified);
+            logger.trace(@"store_id_cards: Stored '$(id_card.display_name)'");
         }
 
         var text = key_file.to_data(null);
@@ -177,6 +203,7 @@ public class LocalFlatFileStore : Object, IIdentityCardStore {
         try {
             var path = get_data_dir();
             var filename = Path.build_filename(path, FILE_NAME);
+            logger.trace("store_id_cards: attempting to store to " + filename);
             var file  = File.new_for_path(filename);
             var stream = file.replace(null, false, FileCreateFlags.PRIVATE);
             #if GIO_VAPI_USES_ARRAYS
@@ -187,6 +214,7 @@ public class LocalFlatFileStore : Object, IIdentityCardStore {
             #endif
                 }
         catch (Error e) {
+            logger.error("store_id_cards: Error while saving keyfile: %s\n".printf(e.message));
             stdout.printf("Error:  %s\n", e.message);
         }
 
index d815210..33796a2 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -38,6 +38,13 @@ public MoonshotLogger get_logger(string name) {
 
 #if USE_LOG4VALA
 
+static void glib_default_log_handler(string? log_domain, LogLevelFlags log_level, string message)
+{
+    Log4Vala.Logger logger = Log4Vala.Logger.get_logger(log_domain ?? "Glib");
+    stderr.printf(log_level.to_string() + " : " + message + "\n");
+    logger.error("Glib error level: " + log_level.to_string() + " : " + message);
+}
+
 /** Logger class that wraps the Log4Vala logger */
 public class MoonshotLogger : Object {
     static bool logger_is_initialized = false;
@@ -46,6 +53,8 @@ public class MoonshotLogger : Object {
 
     public MoonshotLogger(string name) {
         if (!logger_is_initialized) {
+            Log.set_default_handler(glib_default_log_handler);
+
             //!! TODO: Don't hard-code the pathname.
             Log4Vala.init("/home/dbreslau/log4vala.conf");
             logger_is_initialized = true;
index e9b74f6..511dc80 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -33,6 +33,8 @@ using Gtk;
 
 class AddPasswordDialog : Dialog
 {
+    private static Gdk.Color white = make_color(65535, 65535, 65535);
+
     private Entry password_entry;
     private CheckButton remember_checkbutton;
 
@@ -46,60 +48,68 @@ class AddPasswordDialog : Dialog
 
     public AddPasswordDialog(IdCard id_card, IdentityRequest? request)
     {
-        this.set_title(_("Please enter password for ") + id_card.display_name);
+        this.set_title(_("Moonshot - Password"));
         this.set_modal(true);
+        this.modify_bg(StateType.NORMAL, white);
+
+        this.add_buttons(_("Cancel"), ResponseType.CANCEL,
+                         _("Connect"), ResponseType.OK);
 
-        if (request != null) {
-            this.add_buttons(_("Send"), ResponseType.OK,
-                             _("Return to application"), ResponseType.CANCEL);
-        } else {
-            this.add_buttons(_("Done"), ResponseType.OK,
-                             _("Cancel"), ResponseType.CANCEL);
-        }
         this.set_default_response(ResponseType.OK);
 
         var content_area = this.get_content_area();
         ((Box) content_area).set_spacing(12);
-        Label service_label = null;
-        Label service_value = null;
-        if (request != null) {
-            service_label = new Label(_("for use with:"));
-            service_label.set_alignment(1, (float) 0.5);
-            service_value = new Label(request.service);
-            service_value.set_alignment(0, (float) 0.5);
-        }
-
-        var nai_label = new Label(_("Network Access Identifier:"));
-        nai_label.set_alignment(1, (float) 0.5);
+        content_area.modify_bg(StateType.NORMAL, white);
+
+        Label dialog_label = new Label(_("Enter the password for " + id_card.display_name));
+        dialog_label.set_alignment(0, 0);
+
+        var nai_label = new Label(_("User (NAI):"));
+        nai_label.set_alignment(0, 1);
         var nai_value = new Label(id_card.nai);
-        nai_value.set_alignment(0, (float) 0.5);
+        nai_value.set_alignment(0, 0);
 
         var password_label = new Label(_("Password:"));
-        password_label.set_alignment(1, (float) 0.5);
+        password_label.set_alignment(0, (float) 1);
         this.password_entry = new Entry();
         password_entry.set_invisible_char('*');
         password_entry.set_visibility(false);
         password_entry.activates_default = true;
         remember_checkbutton = new CheckButton.with_label(_("Remember password"));
 
-        set_atk_relation(password_entry, password_entry, Atk.RelationType.LABEL_FOR);
+        set_atk_relation(password_label, password_entry, Atk.RelationType.LABEL_FOR);
 
-        var table = new Table(4, 2, false);
+        var table = new Table(6, 1, false);
+        AttachOptions opts = AttachOptions.EXPAND | AttachOptions.FILL;
         int row = 0;
-        table.set_col_spacings(10);
-        table.set_row_spacings(10);
-        if (request != null) {
-            table.attach_defaults(service_label, 0, 1, row, row + 1);
-            table.attach_defaults(service_value, 1, 2, row, row + 1);
-            row++;
-        }
-        table.attach_defaults(nai_label, 0, 1, row, row+1);
-        table.attach_defaults(nai_value, 1, 2, row, row+1);
+        table.set_col_spacings(6);
+        table.set_row_spacings(0);
+        table.attach(dialog_label, 0, 1, row, row + 1, opts, opts, 0, 2);
+//            table.attach_defaults(service_value, 1, 2, row, row + 1);
         row++;
-        table.attach_defaults(password_label, 0, 1, row, row+1);
-        table.attach_defaults(password_entry, 1, 2, row, row+1);
+
+        VBox nai_vbox = new VBox(false, 0);
+        nai_vbox.pack_start(nai_label, false, false, 0);
+        nai_vbox.pack_start(nai_value, false, false, 0);
+        table.attach(nai_vbox, 0, 1, row, row + 1, opts, opts, 0, 12);
+        row++;
+
+        VBox password_vbox = new VBox(false, 1);
+        var empty_box2 = new VBox(false, 0);
+        empty_box2.set_size_request(0, 0);
+        password_vbox.pack_start(empty_box2, false, false, 3);
+        password_vbox.pack_start(password_label, false, false, 0);
+        password_vbox.pack_start(password_entry, false, false, 0);
+        table.attach(password_vbox, 0, 1, row, row + 1, opts, opts, 0, 0);
+        row++;
+
+        table.attach(remember_checkbutton,  0, 1, row, row + 1, opts, opts, 20, 2);
+        row++;
+
+        var empty_box3 = new VBox(false, 0);
+        empty_box3.set_size_request(0, 0);
+        table.attach(empty_box3,  0, 1, row, row + 1, opts, opts, 0, 10);
         row++;
-        table.attach_defaults(remember_checkbutton,  1, 2, row, row+1);
 
         var vbox = new VBox(false, 0);
         vbox.set_border_width(6);
@@ -111,12 +121,4 @@ class AddPasswordDialog : Dialog
         //this.set_resizable(false);
         this.show_all();
     }
-
-    private void set_atk_relation(Widget widget, Widget target_widget, Atk.RelationType relationship)
-    {
-        var atk_widget = widget.get_accessible();
-        var atk_target_widget = target_widget.get_accessible();
-
-        atk_widget.add_relationship(relationship, atk_target_widget);
-    }
 }
index 10e2366..29160dd 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
 
 
 namespace WebProvisioning
-{ 
+{
     bool check_stack(SList<string> stack, string[] reference) {
 
         if (stack.length() < reference.length)
             return false;
-    
+
         for (int i = 0; i < reference.length; i++)
         {
             if (stack.nth_data(i) != reference[i])
@@ -51,74 +51,74 @@ namespace WebProvisioning
     bool always_confirm_handler(SList<string> stack)
     {
         string[] always_confirm_path = {"always-confirm", "rule", "selection-rules", "identity", "identities"};
-    
+
         return check_stack(stack, always_confirm_path);
     }
-  
+
     bool
     pattern_handler(SList<string> stack)
     {
         string[] pattern_path = {"pattern", "rule", "selection-rules", "identity", "identities"};
-    
+
         return check_stack(stack, pattern_path);
     }
 
     bool server_cert_handler(SList<string> stack)
     {
         string[] server_cert_path = {"server-cert", "trust-anchor", "identity", "identities"};
-    
+
         return check_stack(stack, server_cert_path);
     }
 
     bool subject_alt_handler(SList<string> stack)
     {
         string[] subject_alt_path = {"subject-alt", "trust-anchor", "identity", "identities"};
-    
+
         return check_stack(stack, subject_alt_path);
     }
 
     bool subject_handler(SList<string> stack)
     {
         string[] subject_path = {"subject", "trust-anchor", "identity", "identities"};
-    
+
         return check_stack(stack, subject_path);
     }
-  
+
     bool ca_cert_handler(SList<string> stack)
     {
         string[] ca_path = {"ca-cert", "trust-anchor", "identity", "identities"};
-    
+
         return check_stack(stack, ca_path);
     }
 
     bool realm_handler(SList<string> stack)
     {
         string[] realm_path = {"realm", "identity", "identities"};
-    
+
         return check_stack(stack, realm_path);
     }
 
     bool password_handler(SList<string> stack)
     {
         string[] password_path = {"password", "identity", "identities"};
-    
+
         return check_stack(stack, password_path);
     }
 
     bool user_handler(SList<string> stack)
     {
         string[] user_path = {"user", "identity", "identities"};
-    
+
         return check_stack(stack, user_path);
     }
 
     bool display_name_handler(SList<string> stack)
     {
         string[] display_name_path = {"display-name", "identity", "identities"};
-    
+
         return check_stack(stack, display_name_path);
     }
-  
+
     public class Parser : Object
     {
         private static MoonshotLogger logger = new MoonshotLogger("WebProvisioning");
@@ -126,90 +126,106 @@ namespace WebProvisioning
         private void start_element_func(MarkupParseContext context,
                                         string element_name,
                                         string[] attribute_names,
-                                        string[] attribute_values) throws MarkupError 
+                                        string[] attribute_values) throws MarkupError
+        {
+            if (element_name == "identity")
             {
-                if (element_name == "identity")
-                {
-                    logger.trace("start_element_func (%p): Adding an identity".printf(this));
-                    card = new IdCard();
-                    _cards += card;
-                }
-                else if (element_name == "rule")
-                {
-                    card.add_rule(Rule());
-                }
+                card = new IdCard();
+                _cards += card;
+
+                ta_ca_cert = "";
+                ta_server_cert = "";
+                ta_subject = "";
+                ta_subject_alt = "";
+            }
+            else if (element_name == "rule")
+            {
+                card.add_rule(Rule());
             }
+        }
 
-            private void
-            text_element_func(MarkupParseContext context,
-                              string             text,
-                              size_t             text_len) throws MarkupError {
-                unowned SList<string> stack = context.get_element_stack();
-    
-                if (text_len < 1)
-                    return;
-    
-                logger.trace("text_element_func (%p): text='%s'".printf(this, stack.nth_data(0)));
-
-                if (stack.nth_data(0) == "display-name" && display_name_handler(stack))
-                {
-                    card.display_name = text;
-                }
-                else if (stack.nth_data(0) == "user" && user_handler(stack))
-                {
-                    card.username = text;
-                }
-                else if (stack.nth_data(0) == "password" && password_handler(stack))
-                {
-                    card.password = text;
-                }
-                else if (stack.nth_data(0) == "realm" && realm_handler(stack))
-                {
-                    card.issuer = text;
-                }
-                else if (stack.nth_data(0) == "service")
-                {
-                    card.add_service(text);
+        private void end_element_func(MarkupParseContext context,
+                                      string element_name) throws MarkupError
+        {
+            if (element_name == "identity")
+            {
+                if (ta_ca_cert != "" || ta_server_cert != "") {
+                    var ta = new TrustAnchor(ta_ca_cert,
+                                             ta_server_cert,
+                                             ta_subject,
+                                             ta_subject_alt,
+                                             false);
+                    // Set the datetime_added in moonshot-server.vala, since it doesn't get sent via IPC
+                    card.set_trust_anchor_from_store(ta);
                 }
+            }
+        }
+
+        private void
+        text_element_func(MarkupParseContext context,
+                          string             text,
+                          size_t             text_len) throws MarkupError {
+            unowned SList<string> stack = context.get_element_stack();
+
+            if (text_len < 1)
+                return;
+
+            if (stack.nth_data(0) == "display-name" && display_name_handler(stack))
+            {
+                card.display_name = text;
+            }
+            else if (stack.nth_data(0) == "user" && user_handler(stack))
+            {
+                card.username = text;
+            }
+            else if (stack.nth_data(0) == "password" && password_handler(stack))
+            {
+                card.password = text;
+            }
+            else if (stack.nth_data(0) == "realm" && realm_handler(stack))
+            {
+                card.issuer = text;
+            }
+            else if (stack.nth_data(0) == "service")
+            {
+                card.services.add(text);
+            }
 
-                /* Rules */
-                else if (stack.nth_data(0) == "pattern" && pattern_handler(stack))
-                {
-                    /* use temp array to workaround valac 0.10 bug accessing array property length */ 
+            /* Rules */
+            else if (stack.nth_data(0) == "pattern" && pattern_handler(stack))
+            {
+                /* use temp array to workaround valac 0.10 bug accessing array property length */
+                var temp = card.rules;
+                card.rules[temp.length - 1].pattern = text;
+            }
+            else if (stack.nth_data(0) == "always-confirm" && always_confirm_handler(stack))
+            {
+                if (text == "true" || text == "false") {
+                    /* use temp array to workaround valac 0.10 bug accessing array property length*/
                     var temp = card.rules;
-                    card.rules[temp.length - 1].pattern = text;
-                }
-                else if (stack.nth_data(0) == "always-confirm" && always_confirm_handler(stack))
-                {
-                    if (text == "true" || text == "false") {
-                        /* use temp array to workaround valac 0.10 bug accessing array property length*/
-                        var temp = card.rules;
-                        card.rules[temp.length - 1].always_confirm = text;
-                    }
-                }
-                /*Trust anchor*/
-                else if (stack.nth_data(0) == "ca-cert" && ca_cert_handler(stack))
-                {
-                    card.trust_anchor.ca_cert = text;
-                }
-                else if (stack.nth_data(0) == "subject" && subject_handler(stack))
-                {
-                    card.trust_anchor.subject = text;
-                }
-                else if (stack.nth_data(0) == "subject-alt" && subject_alt_handler(stack))
-                {
-                    card.trust_anchor.subject_alt = text;
-                }
-                else if (stack.nth_data(0) == "server-cert" && server_cert_handler(stack))
-                {
-                    card.trust_anchor.server_cert = text;
+                    card.rules[temp.length - 1].always_confirm = text;
                 }
             }
-
-
+            else if (stack.nth_data(0) == "ca-cert" && ca_cert_handler(stack))
+            {
+                ta_ca_cert = text ?? "";
+            }
+            else if (stack.nth_data(0) == "server-cert" && server_cert_handler(stack))
+            {
+                ta_server_cert = text ?? "";
+            }
+            else if (stack.nth_data(0) == "subject" && subject_handler(stack))
+            {
+                ta_subject = text;
+            }
+            else if (stack.nth_data(0) == "subject-alt" && subject_alt_handler(stack))
+            {
+                ta_subject_alt = text;
+            }
+        }
 
         private const MarkupParser parser = {
-            start_element_func, null, text_element_func, null, null
+            start_element_func, end_element_func, text_element_func, null, null
         };
 
         private MarkupParseContext ctx;
@@ -217,6 +233,11 @@ namespace WebProvisioning
         private string       text;
         private string       path;
 
+        private string ta_ca_cert;
+        private string ta_server_cert;
+        private string ta_subject;
+        private string ta_subject_alt;
+
         private IdCard card;
         private IdCard[] _cards = {};
 
@@ -233,20 +254,25 @@ namespace WebProvisioning
             this.path = path;
 
             var file = File.new_for_path(path);
-    
+
             try
             {
                 var dis = new DataInputStream(file.read());
                 string line;
-                while ((line = dis.read_line(null)) != null)
+                while ((line = dis.read_line(null)) != null) {
                     text += line;
+
+                    // Preserve newlines.
+                    //
+                    // This may add an extra newline at EOF. Maybe use
+                    // dis.read_upto("\n", ...) followed by dis.read_byte() instead?
+                    text += "\n";
+                }
             }
             catch(GLib.Error e)
             {
                 error("Could not retreive file size");
             }
-      
-            logger.trace(@"Parser(): read text to parse; length=$(text.length)");
         }
 
         public void parse() {
@@ -257,7 +283,7 @@ namespace WebProvisioning
             catch(GLib.Error e)
             {
                 error("Could not parse %s, invalid content", path);
-            } 
+            }
         }
     }
 }
index b1cc9ba..4fbcd2a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -29,6 +29,9 @@
  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
 */
+
+using Gee;
+
 #if IPC_DBUS
 
 [DBus (name = "org.janet.Moonshot")]
@@ -92,7 +95,7 @@ public class MoonshotServer : Object {
 
         var id_card = request.id_card;
 
-        if ((id_card != null) && (id_card.display_name != IdCard.NO_IDENTITY)) {
+        if ((id_card != null) && (!id_card.is_no_identity())) {
             nai_out = id_card.nai;
             if ((request.password != null) && (request.password != ""))
                 password_out = request.password;
@@ -117,7 +120,7 @@ public class MoonshotServer : Object {
             if (subject_alt_name_constraint == null)
                 subject_alt_name_constraint = "";
 
-            logger.trace("MoonshotServer.get_identity: returning true");
+            logger.trace(@"MoonshotServer.get_identity: returning with nai_out=$nai_out");
 
             return true;
         }
@@ -197,11 +200,22 @@ public class MoonshotServer : Object {
         if ((password != null) && (password != ""))
             idcard.store_password = true;
         idcard.issuer = realm;
-        idcard.services = services;
-        idcard.trust_anchor.ca_cert = ca_cert;
-        idcard.trust_anchor.subject = subject;
-        idcard.trust_anchor.subject_alt = subject_alt;
-        idcard.trust_anchor.server_cert = server_cert;
+        idcard.update_services(services);
+        var ta = new TrustAnchor(ca_cert, server_cert, subject, subject_alt, false);
+
+        if (!ta.is_empty()) {
+            // We have to set the datetime_added here, because it isn't delivered via IPC.
+            string ta_datetime_added = TrustAnchor.format_datetime_now();
+            ta.set_datetime_added(ta_datetime_added);
+            logger.trace("install_id_card : Set ta_datetime_added for '%s' to '%s'; ca_cert='%s'; server_cert='%s'".printf(idcard.display_name, ta.datetime_added, ta.ca_cert, ta.server_cert));
+        }
+        idcard.set_trust_anchor_from_store(ta);
+
+        logger.trace("install_id_card: Card '%s' has services: '%s'"
+                     .printf(idcard.display_name, idcard.get_services_string("; ")));
+
+        logger.trace(@"Installing IdCard named '$(idcard.display_name)'; ca_cert='$(idcard.trust_anchor.ca_cert)'; server_cert='$(idcard.trust_anchor.server_cert)'");
+
 
         if (rules_patterns.length == rules_always_confirm.length)
         {
@@ -216,7 +230,17 @@ public class MoonshotServer : Object {
             idcard.rules = rules;
         }
 
-        return parent_app.add_identity(idcard, force_flat_file_store!=0);
+        ArrayList<IdCard>? old_duplicates = null;
+        var ret = parent_app.add_identity(idcard, (force_flat_file_store != 0), out old_duplicates);
+
+        if (old_duplicates != null) {
+            // Printing to stdout here is ugly behavior; but it's old behavior that
+            // may be expected. (TODO: Do we need to keep this?)
+            foreach (IdCard id_card in old_duplicates) {
+                stdout.printf("removed duplicate id for '%s'\n", id_card.nai);
+            }
+        }
+        return ret;
     }
 
 
@@ -245,13 +269,25 @@ public class MoonshotServer : Object {
                 }
             } 
 
+
+            // prevent a crash by holding the reference to otherwise
+            // unowned array(?)
+
+            // string[] svcs = card.services.to_array();
+            // string[] svcs = card.services.to_array()[:];
+            string[] svcs = new string[card.services.size];
+            for (int i = 0; i < card.services.size; i++) {
+                svcs[i] = card.services[i];
+            }
+
+            logger.trace(@"install_from_file: Adding card with display name '$(card.display_name)'");
             result = install_id_card(card.display_name,
                                      card.username,
                                      card.password,
                                      card.issuer,
                                      rules_patterns,
                                      rules_always_confirm,
-                                     card.services,
+                                     svcs,
                                      card.trust_anchor.ca_cert,
                                      card.trust_anchor.subject,
                                      card.trust_anchor.subject_alt,
@@ -481,10 +517,14 @@ public class MoonshotServer : Object {
 
         mutex.lock();
 
+        ArrayList<IdCard>? old_duplicates = null;
         // Defer addition to the main loop thread.
         Idle.add(() => {
                 mutex.lock();
-                success = parent_app.add_identity(idcard, force_flat_file_store);
+                success = parent_app.add_identity(idcard, force_flat_file_store, out old_duplicates);
+                foreach (IdCard id_card in old_duplicates) {
+                    stdout.printf("removing duplicate id for '%s'\n", new_card.nai);
+                }
                 cond.signal();
                 mutex.unlock();
                 return false;
diff --git a/src/moonshot-settings.vala b/src/moonshot-settings.vala
new file mode 100644 (file)
index 0000000..4dea622
--- /dev/null
@@ -0,0 +1,201 @@
+/*
+ * Copyright (c) 2011-2016, JANET(UK)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of JANET(UK) nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+*/
+
+using Gtk;
+
+   
+private MoonshotLogger logger()
+{
+    return get_logger("MoonshotSettings");
+}
+
+static const string KEY_FILE_NAME="moonshot-ui.config";
+
+private KeyFile get_keyfile()
+{
+    KeyFile key_file = new KeyFile();
+    string config_dir = Environment.get_user_config_dir();
+    logger().trace("get_keyfile: config_dir=" + config_dir);
+
+    File dir = File.new_for_path(config_dir);
+    string path = dir.get_child(KEY_FILE_NAME).get_path();
+
+    try {
+        if (key_file.load_from_file(path, KeyFileFlags.NONE))
+            logger().trace("get_keyfile: load_from_file returned successfully");
+        else
+            logger().trace("get_keyfile: load_from_file returned false");            
+    }
+    catch (FileError e) {
+        logger().trace("get_keyfile: FileError: " + e.message);
+    }
+    catch (KeyFileError e) {
+        logger().trace("get_keyfile: KeyFileError: " + e.message);
+    }
+
+    return key_file;
+}
+
+
+private void save_keyfile(KeyFile key_file)
+{
+    string config_dir = Environment.get_user_config_dir();
+    File dest = null;
+
+    // Make the directory if it doesn't already exist; ignore errors.
+       try {
+               File dir = File.new_for_path(config_dir);
+        dest = dir.get_child(KEY_FILE_NAME);
+               dir.make_directory_with_parents();
+       } catch (Error e) {
+        logger().trace("save_keyfile: make_directory_with_parents threw error (this is usually ignorable) : " + e.message);
+       }
+
+    // It would be nice to use key_file.save_to_file, but the binding doesn't exist
+    // in earlier versions of valac
+    // key_file.save_to_file(path.get_path());
+
+    string data = key_file.to_data();
+    try {
+        logger().trace("save_keyfile: saving to file path '%s'".printf(dest.get_path()));
+        // FileOutputStream s = dest.create(FileCreateFlags.REPLACE_DESTINATION | FileCreateFlags.PRIVATE);
+        // var ds = new DataOutputStream(s);
+        // ds.put_string(data);
+        string new_etag;
+        dest.replace_contents(data.data, null, false, FileCreateFlags.REPLACE_DESTINATION | FileCreateFlags.PRIVATE, out new_etag);
+    }
+    catch(Error e) {
+        logger().error("save_keyfile: error when writing to file: " + e.message);
+    }
+
+    // streams close automatically
+}
+
+internal void set_bool_setting(string group_name, string key_name, bool value, KeyFile? key_file=null)
+{
+    KeyFile tmp_key_file = null;
+    if (key_file == null) {
+        // Use tmp_key_file to hold an owned reference (since key_file is unowned)
+        tmp_key_file = get_keyfile();
+        key_file = tmp_key_file;
+    }
+
+    key_file.set_boolean(group_name, key_name, value);
+
+    if (tmp_key_file != null) {
+        // This is a "one-shot" settings update; save it now.
+        save_keyfile(key_file);
+    }
+}
+
+internal bool get_bool_setting(string group_name, string key_name, bool default=false, KeyFile? key_file=null)
+{
+    KeyFile tmp_key_file = null;
+    if (key_file == null) {
+        // Use tmp_key_file to hold an owned reference (since key_file is unowned)
+        tmp_key_file = get_keyfile();
+        key_file = tmp_key_file;
+    }
+
+    if (key_file == null)
+        return default;
+
+    try {
+        if (!key_file.has_key(group_name, key_name))
+        {
+            logger().info(@"get_bool_setting : key file doesn't contain key '$key_name' in group '$group_name'");
+            return default;
+        }
+    }
+    catch(KeyFileError e) {
+        logger().info(@"get_bool_setting : KeyFileError checking if key '$key_name' exists in group '$group_name' (maybe ignorable?) : " + e.message);
+    }
+
+    try {
+        // throws KeyFileError if key is not found
+        return key_file.get_boolean(group_name, key_name);
+    }
+    catch (KeyFileError e) {
+        logger().info("get_bool_setting got KeyFileError (may be ignorable) : " + e.message);
+    }
+    return default;
+}
+
+
+internal void set_string_setting(string group_name, string key_name, string value, KeyFile? key_file=null)
+{
+    KeyFile tmp_key_file = null;
+    if (key_file == null) {
+        // Use tmp_key_file to hold an owned reference (since key_file is unowned)
+        tmp_key_file = get_keyfile();
+        key_file = tmp_key_file;
+    }
+
+    key_file.set_string(group_name, key_name, value);
+    if (tmp_key_file != null) {
+        // This is a "one-shot" settings update; save it now.
+        save_keyfile(key_file);
+    }
+}
+
+internal string get_string_setting(string group_name, string key_name, string default="", KeyFile? key_file=null)
+{
+    KeyFile tmp_key_file = null;
+    if (key_file == null) {
+        // Use tmp_key_file to hold an owned reference (since key_file is unowned)
+        tmp_key_file = get_keyfile();
+        key_file = tmp_key_file;
+    }
+
+    if (key_file == null)
+        return default;
+
+    try {
+        if (!key_file.has_key(group_name, key_name))
+        {
+            logger().info(@"get_string_setting : key file doesn't contain key '$key_name' in group '$group_name'");
+            return default;
+        }
+    }
+    catch(KeyFileError e) {
+        logger().info(@"get_string_setting : KeyFileError checking if key '$key_name' exists in group '$group_name' (maybe ignorable?) : " + e.message);
+    }
+
+    try {
+        // throws KeyFileError if key is not found
+        return key_file.get_string(group_name, key_name);
+    }
+    catch (KeyFileError e) {
+        logger().info("get_string_setting got KeyFileError (may be ignorable) : " + e.message);
+    }
+    return default;
+}
diff --git a/src/moonshot-trust-anchor-dialog.vala b/src/moonshot-trust-anchor-dialog.vala
new file mode 100644 (file)
index 0000000..0537494
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2011-2016, JANET(UK)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of JANET(UK) nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+*/
+using Gtk;
+
+class TrustAnchorDialog : Dialog
+{
+    private static Gdk.Color white = make_color(65535, 65535, 65535);
+
+    public bool complete = false;
+
+    public TrustAnchorDialog(IdCard idcard, Window parent)
+    {
+        this.set_title(_("Trust Anchor"));
+        this.set_modal(true);
+        this.set_transient_for(parent);
+        this.modify_bg(StateType.NORMAL, white);
+
+        this.add_buttons(_("Cancel"), ResponseType.CANCEL,
+                         _("Confirm"), ResponseType.OK);
+
+        this.set_default_response(ResponseType.OK);
+
+        var content_area = this.get_content_area();
+        ((Box) content_area).set_spacing(12);
+        content_area.modify_bg(StateType.NORMAL, white);
+
+        Label dialog_label = new Label("");
+        dialog_label.set_alignment(0, 0);
+
+        string label_markup = "<span font-weight='heavy'>" + _("You are using this identity for the first time with the following trust anchor:") + "</span>";
+
+        dialog_label.set_markup(label_markup);
+        dialog_label.set_line_wrap(true);
+        dialog_label.set_width_chars(60);
+                                                   
+        var user_label = new Label(_("Username: ") + idcard.username);
+        user_label.set_alignment(0, 0.5f);
+
+        var realm_label = new Label(_("Realm: ") + idcard.issuer);
+        realm_label.set_alignment(0, 0.5f);
+
+        Label confirm_label = new Label(_("Please confirm that this is the correct trust anchor."));
+        confirm_label.set_alignment(0, 0.5f);
+
+        var trust_anchor_display = make_ta_fingerprint_widget(idcard.trust_anchor);
+
+        var vbox = new VBox(false, 0);
+        vbox.set_border_width(6);
+        vbox.pack_start(dialog_label, true, true, 12);
+        vbox.pack_start(user_label, true, true, 2);
+        vbox.pack_start(realm_label, true, true, 2);
+        vbox.pack_start(trust_anchor_display, true, true, 0);
+        vbox.pack_start(confirm_label, true, true, 12);
+
+        ((Container) content_area).add(vbox);
+
+        this.set_border_width(6);
+        this.set_resizable(false);
+
+        this.response.connect(on_response);
+
+        this.show_all();
+    }
+
+    private void on_response(Dialog source, int response_id)
+    {
+        switch (response_id) {
+        case ResponseType.OK:
+            complete = true;
+            break;
+        case ResponseType.CANCEL:
+            complete = true;
+            break;
+        }
+    }
+}
index 8bc2a68..4eaed1f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
 */
+
+using Gtk;
+using Pango;
+
 #if OS_WIN32
 extern string? g_win32_get_package_installation_directory_of_module(void *module);
 #endif
@@ -100,3 +104,72 @@ public bool UserForcesFlatFileStore()
     }
     return false;
 }
+
+internal Gdk.Color make_color(uint16 red, uint16 green, uint16 blue)
+{
+    Gdk.Color color = Gdk.Color();
+    color.red = red;
+    color.green = green;
+    color.blue = blue;
+
+    return color;
+}
+
+internal void set_atk_relation(Widget widget, Widget target_widget, Atk.RelationType relationship)
+{
+    var atk_widget = widget.get_accessible();
+    var atk_target_widget = target_widget.get_accessible();
+
+    atk_widget.add_relationship(relationship, atk_target_widget);
+}
+
+
+internal Widget make_ta_fingerprint_widget(TrustAnchor trust_anchor)
+{
+        var fingerprint_label = new Label(_("SHA-256 fingerprint:"));
+        fingerprint_label.set_alignment(0, 0.5f);
+
+        var fingerprint = new TextView();
+        var fontdesc = FontDescription.from_string("monospace 10");
+        fingerprint.modify_font(fontdesc);
+        fingerprint.set_editable(false);
+        fingerprint.set_left_margin(3);
+        var buffer = fingerprint.get_buffer();
+        buffer.set_text(colonize(trust_anchor.server_cert, 16), -1);
+        fingerprint.wrap_mode = Gtk.WrapMode.WORD_CHAR;
+
+        set_atk_relation(fingerprint_label, fingerprint, Atk.RelationType.LABEL_FOR);
+
+        var fingerprint_width_constraint = new ScrolledWindow(null, null);
+        fingerprint_width_constraint.set_policy(PolicyType.NEVER, PolicyType.NEVER);
+        fingerprint_width_constraint.set_shadow_type(ShadowType.IN);
+        fingerprint_width_constraint.set_size_request(300, 60);
+        fingerprint_width_constraint.add_with_viewport(fingerprint);
+
+        var vbox = new VBox(false, 0);
+        vbox.pack_start(fingerprint_label, true, true, 2);
+        vbox.pack_start(fingerprint_width_constraint, true, true, 2);
+        return vbox;
+}
+
+    // Yeah, it doesn't mean "colonize" the way you might think... :-)
+internal static string colonize(string input, int bytes_per_line) {
+    return_if_fail(input.length % 2 == 0);
+
+    string result = "";
+    int i = 0;
+    int line_bytes = 0;
+    while (i < input.length) {
+        if (line_bytes == bytes_per_line) {
+            result += "\n";
+            line_bytes = 0;
+        }
+        else if (i > 0) {
+            result += ":";
+        }
+        result += input[i : i + 2];
+        i += 2;
+        line_bytes++;
+    }
+    return result;
+}
diff --git a/src/moonshot-warning-dialog.vala b/src/moonshot-warning-dialog.vala
new file mode 100644 (file)
index 0000000..72233a4
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2011-2016, JANET(UK)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of JANET(UK) nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+*/
+using Gtk;
+
+static const string GROUP_NAME="WarningDialogs";
+
+// MessageDialog doesn't allow subclassing, so we merely wrap the
+// constructor for it the dialog, and then run it, returning the result.
+class WarningDialog 
+{
+    private static MoonshotLogger _logger = null;
+    private static MoonshotLogger logger()
+        {
+            if (_logger == null) {
+                _logger = get_logger("WarningDialog");
+            }
+            return _logger;
+        }
+
+    public static bool confirm(Window parent, string message, string dialog_name)
+    {
+
+        if (get_bool_setting(GROUP_NAME, dialog_name, false))
+        {
+            logger().trace(@"confirm: Settings group $GROUP_NAME has 'true' for key $dialog_name; skipping dialog and returning true.");
+            return true;
+        }
+
+        Gdk.Color white = make_color(65535, 65535, 65535);
+
+        MessageDialog dialog = new Gtk.MessageDialog(parent,
+                                                     Gtk.DialogFlags.DESTROY_WITH_PARENT,
+                                                     Gtk.MessageType.WARNING,
+                                                     Gtk.ButtonsType.YES_NO,
+                                                     "");
+
+        var content_area = dialog.get_content_area();
+        CheckButton remember_checkbutton = null;
+
+        if (dialog_name != null && dialog_name != "")
+        {
+            remember_checkbutton = new CheckButton.with_label(_("Do not show this message again"));
+            // remember_checkbutton.set_focus_on_click(false);
+            // remember_checkbutton.set_can_focus(false);
+            // remember_checkbutton.has_focus = false;
+            remember_checkbutton.set_receives_default(false);
+            Container action_area = (Container) dialog.get_action_area();
+
+            // This is awful, because it assumes the Yes button is first in the
+            // children (and for that matter, it assumes there are no intermediate
+            // containers.) But searching for "Yes" in the widget text would
+            // cause localization problems.
+            // TODO: Rewrite to use Dialog instead of MessageDialog?
+            var yes_button = action_area.get_children().first().data;
+            yes_button.grab_default();
+            yes_button.grab_focus();
+
+// Not sure if 0.26 is the minimum for MessageDialog.get_message_area. 0.16 sure isn't :-(
+#if VALA_0_26
+            var message_area = dialog.get_message_area();
+            ((Box)message_area).pack_start(remember_checkbutton, false, false, 12);
+#else
+            HBox hbox = new HBox(false, 0);
+            hbox.pack_start(new HBox(false, 0), true, true, 20);
+            hbox.pack_start(remember_checkbutton, false, false, 12);
+            ((Box)content_area).pack_start(hbox, true, true, 12);
+#endif
+        }
+
+        // dialog.set_modal(true);
+        dialog.set_title(_("Warning"));
+        dialog.modify_bg(StateType.NORMAL, white);
+
+        // ((Box) content_area).set_spacing(12);
+        content_area.modify_bg(StateType.NORMAL, white);
+
+        content_area.show_all();
+
+        dialog.set_markup(message);
+
+        var ret = dialog.run();
+
+        if (ret == Gtk.ResponseType.YES && remember_checkbutton != null && remember_checkbutton.active)
+        {
+            set_bool_setting(GROUP_NAME, dialog_name, true);
+        }
+
+        dialog.destroy();
+        return (ret == Gtk.ResponseType.YES);
+    }
+}
index 90b5452..de52aff 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2014, JANET(UK)
+ * Copyright (c) 2011-2016, JANET(UK)
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -37,7 +37,7 @@ namespace WebProvisioning
 
     public static int main(string[] args)
     {
-        logger = new MoonshotLogger("WebProvisioning");
+        logger = new MoonshotLogger("WebProvisioning (WebpParser)");
 
         int arg_index = -1;
         int force_flat_file_store = 0;
@@ -90,7 +90,11 @@ namespace WebProvisioning
         
             /* use temp arrays to workaround centos array property bug */
             var rules = card.rules;
-            var services = card.services;
+            string[] svcs = new string[card.services.size];
+            for (int i = 0; i < card.services.size; i++) {
+                svcs[i] = card.services[i];
+            }
+
             if (rules.length > 0)
             {
                 int i = 0;
@@ -111,7 +115,7 @@ namespace WebProvisioning
                                      card.issuer,
                                      rules_patterns,
                                      rules_always_confirm,
-                                     services,
+                                     svcs,
                                      card.trust_anchor.ca_cert,
                                      card.trust_anchor.subject,
                                      card.trust_anchor.subject_alt,
index 2c11172..bf6319a 100644 (file)
@@ -1,12 +1,21 @@
 <identities>
+    <identity>
+    <display-name>A Simple Card</display-name>
+    <user>user5</user>
+    <password></password>
+    <realm>painless-security.com</realm>
+    <services>
+    </services>
+  </identity>
+
   <identity>
     <display-name>Unique Name</display-name>
     <user>user1</user>
     <password></password>
-    <realm>foo.baz</realm>
+    <realm>painless-security.com</realm>
     <services>
-      <service>irc@jabber.project-moonshot.org</service>
-      <service>xmpp@jabber.project-moonshot.org</service>
+      <service>irc/painless-security.com</service>
+      <service>xmpp/painless-security.com</service>
     </services>
     <selection-rules>
       <rule>
         <always-confirm>true</always-confirm>
       </rule>
       <rule>
-        <pattern>imap@*moonshot.org</pattern>
+        <pattern>imap/*moonshot.org</pattern>
         <always-confirm>false</always-confirm>
       </rule>
     </selection-rules>
     <trust-anchor>
-      <ca-cert>ABCDEFGHIJKLMNOPQRSTUVWXYZ123455678910</ca-cert>
-      <subject>Foo</subject>
-      <subject-alt>Bar</subject-alt>
+     <!-- PEM encoded, minus header and footer -->
+      <ca-cert>MIIE9jCCA96gAwIBAgIJAJ6SVDCP6o2nMA0GCSqGSIb3DQEBBQUAMIGaMQswCQYD
+VQQGEwJVUzELMAkGA1UECBMCTUExDzANBgNVBAcTBk1hbGRlbjEaMBgGA1UEChMR
+UGFpbmxlc3MgU2VjdXJpdHkxLzAtBgkqhkiG9w0BCQEWIHBvc3RtYXN0ZXJAcGFp
+bmxlc3Mtc2VjdXJpdHkuY29tMSAwHgYDVQQDExdQYWlubGVzcyBTZWN1cml0eSwg
+SW5jLjAeFw0xNjA4MDExNjIxMDVaFw0xOTExMTQxNjIxMDVaMIGaMQswCQYDVQQG
+EwJVUzELMAkGA1UECBMCTUExDzANBgNVBAcTBk1hbGRlbjEaMBgGA1UEChMRUGFp
+bmxlc3MgU2VjdXJpdHkxLzAtBgkqhkiG9w0BCQEWIHBvc3RtYXN0ZXJAcGFpbmxl
+c3Mtc2VjdXJpdHkuY29tMSAwHgYDVQQDExdQYWlubGVzcyBTZWN1cml0eSwgSW5j
+LjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKPiSkw1y6zMJFjnoPjd
+5Bh9EA1NhQcoNxJAtgYEJtpH9a2tfjnXXncXpbIMIfMgv2VKRAxvKb+knCfSCRtU
+PM9i998+ZhJY9o6SSFomlMvdaClauPvBhQvQMmJmp1WINgMUHPpzsGlj04kkl7jw
+iK/oDxp1becikKc10Gr9W03aEJtOaiSqC45zeIgnz9GoQ2tJvz2DDBcddaaT1mSV
+n/lk4ahPC4XaJ08Jn1L6XkVVyDGD38Rwg7r1SFI7ByBFvvQh93Fa48Z7ik0I8s48
+U1euHak2gSJ4zfzLndvGy05qMjhRTlxQu+Rt1g7CS3CLcJqqYzWNrEJWpD8Wn7iA
+MIUCAwEAAaOCATswggE3MB0GA1UdDgQWBBR1qlvY7r2DqhHu5s+sCUPeqBcQuzCB
+zwYDVR0jBIHHMIHEgBR1qlvY7r2DqhHu5s+sCUPeqBcQu6GBoKSBnTCBmjELMAkG
+A1UEBhMCVVMxCzAJBgNVBAgTAk1BMQ8wDQYDVQQHEwZNYWxkZW4xGjAYBgNVBAoT
+EVBhaW5sZXNzIFNlY3VyaXR5MS8wLQYJKoZIhvcNAQkBFiBwb3N0bWFzdGVyQHBh
+aW5sZXNzLXNlY3VyaXR5LmNvbTEgMB4GA1UEAxMXUGFpbmxlc3MgU2VjdXJpdHks
+IEluYy6CCQCeklQwj+qNpzAMBgNVHRMEBTADAQH/MDYGA1UdHwQvMC0wK6ApoCeG
+JWh0dHA6Ly93d3cuZXhhbXBsZS5jb20vZXhhbXBsZV9jYS5jcmwwDQYJKoZIhvcN
+AQEFBQADggEBAB6J5Zxvq96SdIsfEajqU+pANBiA2VTZCpxfIMAKz8KfyzWzFvCM
+8epvYDliyOjw1zR9cYxhQqOcbPHrjLXheVvCePd3jCUOv+tt1Nw2gS2DiMuq37DO
+BZOTlPJ3m2NnvJVO3NjB2I+Pk9v3YlG6mkiVc9dNWgO20SqT2Y+KvHqA5Of8Cb/s
+uIBftctvGpIyEnqSmU7KB0nhIWe65Bsu60hjHHfX1qhJE7qGKbqNaHujssQ/SBXJ
+g7HUhtywv8z3TFoYW0MoBpKGM2Ojc9kQ8f0rYvUKTiD1UfjQoll/Io5xwKy7FXtn
+musuCxXeWkqDtw0clWg6vkf5Tb9v/JQ2PW0=</ca-cert>
+      <subject>Painless Security Server Certificate</subject>
       <!-- Or alternatively -->
-      <server-cert>ABCDEFGHIJKLMNOPQRSTUVWXYZ123455678910</server-cert>
+      <server-cert></server-cert>
     </trust-anchor>
   </identity>
   <identity>
     <display-name>Another Unique Name</display-name>
     <user>user2</user>
     <password></password>
-    <realm>foo.bar</realm>
+    <realm>painless-security.com</realm>
     <services>
-      <service>irc@jabber.project-moonshot.org</service>
-      <service>email@project-moonshot.org</service>
+      <service>irc/painless-security.com</service>
+      <service>email/painless-security.com</service>
     </services>
     <selection-rules>
       <rule>
-        <pattern>*@project-moonshot.org</pattern>
+        <pattern>*/painless-security.com</pattern>
         <always-confirm>true</always-confirm>
       </rule>
     </selection-rules>
     <trust-anchor>
-      <ca-cert>ABCDEFGHIJKLMNOPQRSTUVWXYZ123455678910</ca-cert>
-      <subject>Foo</subject>
-      <subject-alt>Bar</subject-alt>
-      <!-- Or alternatively -->
-      <server-cert>ABCDEFGHIJKLMNOPQRSTUVWXYZ123455678910</server-cert>
+      <ca-cert>
+      <!-- DER format, base64-encoded -->MIIE9jCCA96gAwIBAgIJAJ6SVDCP6o2nMA0GCSqGSIb3DQEBBQUAMIGaMQswCQYDVQQGEwJVUzEL
+MAkGA1UECBMCTUExDzANBgNVBAcTBk1hbGRlbjEaMBgGA1UEChMRUGFpbmxlc3MgU2VjdXJpdHkx
+LzAtBgkqhkiG9w0BCQEWIHBvc3RtYXN0ZXJAcGFpbmxlc3Mtc2VjdXJpdHkuY29tMSAwHgYDVQQD
+ExdQYWlubGVzcyBTZWN1cml0eSwgSW5jLjAeFw0xNjA4MDExNjIxMDVaFw0xOTExMTQxNjIxMDVa
+MIGaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCTUExDzANBgNVBAcTBk1hbGRlbjEaMBgGA1UEChMR
+UGFpbmxlc3MgU2VjdXJpdHkxLzAtBgkqhkiG9w0BCQEWIHBvc3RtYXN0ZXJAcGFpbmxlc3Mtc2Vj
+dXJpdHkuY29tMSAwHgYDVQQDExdQYWlubGVzcyBTZWN1cml0eSwgSW5jLjCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBAKPiSkw1y6zMJFjnoPjd5Bh9EA1NhQcoNxJAtgYEJtpH9a2tfjnX
+XncXpbIMIfMgv2VKRAxvKb+knCfSCRtUPM9i998+ZhJY9o6SSFomlMvdaClauPvBhQvQMmJmp1WI
+NgMUHPpzsGlj04kkl7jwiK/oDxp1becikKc10Gr9W03aEJtOaiSqC45zeIgnz9GoQ2tJvz2DDBcd
+daaT1mSVn/lk4ahPC4XaJ08Jn1L6XkVVyDGD38Rwg7r1SFI7ByBFvvQh93Fa48Z7ik0I8s48U1eu
+Hak2gSJ4zfzLndvGy05qMjhRTlxQu+Rt1g7CS3CLcJqqYzWNrEJWpD8Wn7iAMIUCAwEAAaOCATsw
+ggE3MB0GA1UdDgQWBBR1qlvY7r2DqhHu5s+sCUPeqBcQuzCBzwYDVR0jBIHHMIHEgBR1qlvY7r2D
+qhHu5s+sCUPeqBcQu6GBoKSBnTCBmjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAk1BMQ8wDQYDVQQH
+EwZNYWxkZW4xGjAYBgNVBAoTEVBhaW5sZXNzIFNlY3VyaXR5MS8wLQYJKoZIhvcNAQkBFiBwb3N0
+bWFzdGVyQHBhaW5sZXNzLXNlY3VyaXR5LmNvbTEgMB4GA1UEAxMXUGFpbmxlc3MgU2VjdXJpdHks
+IEluYy6CCQCeklQwj+qNpzAMBgNVHRMEBTADAQH/MDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly93
+d3cuZXhhbXBsZS5jb20vZXhhbXBsZV9jYS5jcmwwDQYJKoZIhvcNAQEFBQADggEBAB6J5Zxvq96S
+dIsfEajqU+pANBiA2VTZCpxfIMAKz8KfyzWzFvCM8epvYDliyOjw1zR9cYxhQqOcbPHrjLXheVvC
+ePd3jCUOv+tt1Nw2gS2DiMuq37DOBZOTlPJ3m2NnvJVO3NjB2I+Pk9v3YlG6mkiVc9dNWgO20SqT
+2Y+KvHqA5Of8Cb/suIBftctvGpIyEnqSmU7KB0nhIWe65Bsu60hjHHfX1qhJE7qGKbqNaHujssQ/
+SBXJg7HUhtywv8z3TFoYW0MoBpKGM2Ojc9kQ8f0rYvUKTiD1UfjQoll/Io5xwKy7FXtnmusuCxXe
+WkqDtw0clWg6vkf5Tb9v/JQ2PW0=
+</ca-cert>
+      <subject>Painless Security Server Certificate</subject>
     </trust-anchor>
   </identity>
     <identity>
     <display-name>Yet Another Unique Name</display-name>
     <user>user3</user>
     <password></password>
-    <realm>foo.com</realm>
+    <realm>painless-security.com</realm>
     <services>
-      <service>irc@jabber.project-moonshot.org</service>
-      <service>email@project-moonshot.org</service>
+      <service>irc/painless-security.com</service>
+      <service>email/painless-security.com</service>
     </services>
     <trust-anchor>
-      <ca-cert>ABCDEFGHIJKLMNOPQRSTUVWXYZ123455678910</ca-cert>
-      <subject>Foo</subject>
-      <subject-alt>Bar</subject-alt>
-      <!-- Or alternatively -->
-      <server-cert>ABCDEFGHIJKLMNOPQRSTUVWXYZ123455678910</server-cert>
+      <server-cert>3838E17EC9A2A06D7B6030E3C5727E3466EAB4BB4159DCE7CF6297ADAFC8A56F</server-cert>
+    </trust-anchor>
+  </identity>
+    <identity>
+    <display-name>A Not Really Unique Name</display-name>
+    <user>user4</user>
+    <password></password>
+    <realm>painless-security.com</realm>
+    <services>
+      <service>ssh/painless-security.com</service>
+      <service>email/painless-security.com</service>
+    </services>
+    <trust-anchor>
+      <server-cert>3838E17EC9A2A06D7B6030E3C5727E3466EAB4BB4159DCE7CF6297ADAFC8A56F</server-cert>
     </trust-anchor>
   </identity>
+
 </identities>