[seahorse] Ported the SSH module to Vala.
- From: Niels De Graef <nielsdg src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [seahorse] Ported the SSH module to Vala.
- Date: Sat, 30 Sep 2017 00:58:32 +0000 (UTC)
commit 8fe1c6d99462e296a8ddedd2cf7dd658f2f26a9f
Author: Niels De Graef <nielsdegraef gmail com>
Date: Fri Jan 13 20:51:57 2017 +0100
Ported the SSH module to Vala.
* Added separate file for the type of algorithm: `algorithm.vala`
This should facilitate adding other encryption types.
* Added SSH-specific errors: `errors.vala`. Just a stump right now, but
we should be able to expand that when the time comes.
* The dialogs no longer use Seahorse.ObjectWidget/Seahorse.Widget. (In
the long term, we'll probably deprecate these)
* An Operation is an object, which provides us a convenient closure.
* Added a lot more comments.
* Some types of base classes should've been nullable, but weren't.
common/actions.vala | 2 +-
common/backend.vala | 4 +-
common/config.vapi | 39 ++
gkr/gkr-backend.vala | 4 +-
po/POTFILES.in | 24 +-
ssh/Makefile.am | 70 ++-
ssh/actions.vala | 78 +++
ssh/algorithm.vala | 87 +++
ssh/backend.vala | 84 +++
ssh/deleter.vala | 123 ++++
ssh/errors.vala | 21 +
ssh/exporter.vala | 130 ++++
ssh/generate.vala | 155 +++++
ssh/key-data.vala | 255 ++++++++
ssh/key-properties.vala | 198 +++++++
ssh/key.vala | 340 +++++++++++
ssh/operation.vala | 397 +++++++++++++
ssh/seahorse-ssh-actions.c | 133 -----
ssh/seahorse-ssh-actions.h | 29 -
ssh/seahorse-ssh-askpass.c | 1 +
ssh/seahorse-ssh-backend.c | 273 ---------
ssh/seahorse-ssh-backend.h | 47 --
ssh/seahorse-ssh-deleter.c | 204 -------
ssh/seahorse-ssh-deleter.h | 41 --
ssh/seahorse-ssh-dialogs.h | 20 -
ssh/seahorse-ssh-exporter.c | 331 -----------
ssh/seahorse-ssh-exporter.h | 40 --
ssh/seahorse-ssh-generate.c | 221 -------
ssh/seahorse-ssh-generate.ui | 509 ++++++++---------
ssh/seahorse-ssh-key-data.c | 453 --------------
ssh/seahorse-ssh-key-data.h | 103 ----
ssh/seahorse-ssh-key-properties.c | 382 ------------
ssh/seahorse-ssh-key-properties.ui | 806 ++++++++++++--------------
ssh/seahorse-ssh-key.c | 459 ---------------
ssh/seahorse-ssh-key.h | 92 ---
ssh/seahorse-ssh-operation.c | 1140 ------------------------------------
ssh/seahorse-ssh-operation.h | 116 ----
ssh/seahorse-ssh-source.c | 1008 -------------------------------
ssh/seahorse-ssh-source.h | 96 ---
ssh/seahorse-ssh-upload.c | 185 ------
ssh/seahorse-ssh-upload.ui | 271 ++++------
ssh/seahorse-ssh.h | 39 --
ssh/source.vala | 462 +++++++++++++++
ssh/{seahorse-ssh.c => ssh.vala} | 25 +-
ssh/upload.vala | 144 +++++
45 files changed, 3300 insertions(+), 6341 deletions(-)
---
diff --git a/common/actions.vala b/common/actions.vala
index 1e4637c..08cd7bb 100644
--- a/common/actions.vala
+++ b/common/actions.vala
@@ -65,7 +65,7 @@ public class Actions : Gtk.ActionGroup {
private unowned string? _definition;
private WeakRef _catalog;
- Actions(string name) {
+ public Actions(string name) {
GLib.Object(
name: name
);
diff --git a/common/backend.vala b/common/backend.vala
index 257cdc8..9f245ec 100644
--- a/common/backend.vala
+++ b/common/backend.vala
@@ -22,10 +22,10 @@ public interface Backend : Gcr.Collection {
public abstract string name { get; }
public abstract string label { get; }
public abstract string description { get; }
- public abstract Gtk.ActionGroup actions { owned get; }
+ public abstract Gtk.ActionGroup? actions { owned get; }
public abstract bool loaded { get; }
- public abstract Place lookup_place(string uri);
+ public abstract Place? lookup_place(string uri);
public void register() {
Registry.register_object(this, "backend");
diff --git a/common/config.vapi b/common/config.vapi
index 32f6d1a..7ce46ba 100644
--- a/common/config.vapi
+++ b/common/config.vapi
@@ -3,9 +3,15 @@ namespace Config
{
public const string PKGDATADIR;
+ public const string EXECDIR;
+
public const string VERSION;
public const string PACKAGE;
public const string GETTEXT_PACKAGE;
+
+ public const bool WITH_SSH;
+ public const string SSH_PATH;
+ public const string SSH_KEYGEN_PATH;
}
/*
@@ -39,4 +45,37 @@ public class Interaction : GLib.TlsInteraction {
public Gtk.Window? parent;
}
+[CCode (cheader_filename = "libseahorse/seahorse-object.h")]
+public class Object : GLib.Object {
+ public Place place { get; }
+ public Actions actions { get; }
+ public string label { get; }
+ public string markup { get; }
+ public string nickname { get; }
+ public GLib.Icon icon { get; }
+ public string identifier { get; }
+ public Usage usage { get; }
+ public Seahorse.Flags flags { get; }
+
+ public Object();
+}
+
+[CCode (cheader_filename = "libseahorse/seahorse-validity.h", cname = "SeahorseValidity", cprefix =
"SEAHORSE_VALIDITY_", has_type_id = false)]
+public enum Validity {
+ REVOKED,
+ DISABLED,
+ NEVER,
+ UNKNOWN,
+ MARGINAL,
+ FULL,
+ ULTIMATE;
+
+ public string? get_string();
+}
+
+[CCode (cheader_filename = "libseahorse/seahorse-progress.h")]
+namespace Progress {
+ public void show(GLib.Cancellable? cancellable, string title, bool delayed);
+}
+
}
diff --git a/gkr/gkr-backend.vala b/gkr/gkr-backend.vala
index b88d35e..9003c5a 100644
--- a/gkr/gkr-backend.vala
+++ b/gkr/gkr-backend.vala
@@ -43,7 +43,7 @@ public class Backend: GLib.Object , Gcr.Collection, Seahorse.Backend {
get { return _("Stored personal passwords, credentials and secrets"); }
}
- public Gtk.ActionGroup actions {
+ public Gtk.ActionGroup? actions {
owned get { return this._actions; }
}
@@ -151,7 +151,7 @@ public class Backend: GLib.Object , Gcr.Collection, Seahorse.Backend {
return false;
}
- public Place lookup_place(string uri) {
+ public Place? lookup_place(string uri) {
return this._keyrings.lookup(uri);
}
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 36341f9..c084bf0 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -93,17 +93,21 @@ src/seahorse-key-manager.c
src/seahorse-key-manager.ui
src/seahorse-main.c
src/seahorse-sidebar.c
-ssh/seahorse-ssh-actions.c
+ssh/actions.vala
+ssh/algorithm.vala
+ssh/backend.vala
+ssh/deleter.vala
+ssh/errors.vala
+ssh/exporter.vala
+ssh/generate.vala
+ssh/key-data.vala
+ssh/key-properties.vala
+ssh/key.vala
+ssh/operation.vala
ssh/seahorse-ssh-askpass.c
-ssh/seahorse-ssh-backend.c
-ssh/seahorse-ssh-deleter.c
-ssh/seahorse-ssh-exporter.c
-ssh/seahorse-ssh-generate.c
ssh/seahorse-ssh-generate.ui
-ssh/seahorse-ssh-key-properties.c
ssh/seahorse-ssh-key-properties.ui
-ssh/seahorse-ssh-key.c
-ssh/seahorse-ssh-operation.c
-ssh/seahorse-ssh-source.c
-ssh/seahorse-ssh-upload.c
ssh/seahorse-ssh-upload.ui
+ssh/source.vala
+ssh/ssh.vala
+ssh/upload.vala
diff --git a/ssh/Makefile.am b/ssh/Makefile.am
index 18069a1..412a7a8 100644
--- a/ssh/Makefile.am
+++ b/ssh/Makefile.am
@@ -1,21 +1,60 @@
noinst_LIBRARIES += libseahorse-ssh.a
+ssh_VALA = \
+ ssh/actions.vala \
+ ssh/algorithm.vala \
+ ssh/backend.vala \
+ ssh/deleter.vala \
+ ssh/errors.vala \
+ ssh/exporter.vala \
+ ssh/generate.vala \
+ ssh/key-data.vala \
+ ssh/key-properties.vala \
+ ssh/key.vala \
+ ssh/operation.vala \
+ ssh/source.vala \
+ ssh/ssh.vala \
+ ssh/upload.vala \
+ $(NULL)
+
+ssh_HEADER = ssh/seahorse-ssh.h
+
+ssh_C = $(ssh_VALA:.vala=.c)
+
libseahorse_ssh_a_SOURCES = \
- ssh/seahorse-ssh.h ssh/seahorse-ssh.c \
- ssh/seahorse-ssh-actions.c ssh/seahorse-ssh-actions.h \
- ssh/seahorse-ssh-backend.c ssh/seahorse-ssh-backend.h \
- ssh/seahorse-ssh-deleter.c ssh/seahorse-ssh-deleter.h \
- ssh/seahorse-ssh-dialogs.h \
- ssh/seahorse-ssh-exporter.c ssh/seahorse-ssh-exporter.h \
- ssh/seahorse-ssh-generate.c \
- ssh/seahorse-ssh-key-data.c ssh/seahorse-ssh-key-data.h \
- ssh/seahorse-ssh-key.c ssh/seahorse-ssh-key.h \
- ssh/seahorse-ssh-key-properties.c \
- ssh/seahorse-ssh-source.c ssh/seahorse-ssh-source.h \
- ssh/seahorse-ssh-operation.c ssh/seahorse-ssh-operation.h \
- ssh/seahorse-ssh-upload.c
+ $(ssh_C) $(ssh_HEADER)
+
+libseahorse_ssh_a_CFLAGS = \
+ -include config.h -w \
+ -DSECRET_API_SUBJECT_TO_CHANGE
+
+if WITH_VALAC
+$(ssh_HEADER): $(ssh_VALA) $(common_VAPI)
+ $(V_VALAC) $(VALAC) $(VALA_FLAGS) -C --use-header --header=$(ssh_HEADER) \
+ --pkg gtk+-3.0 --pkg gcr-3 --pkg gcr-ui-3 --pkg libsecret-1 --pkg posix \
+ --directory=$(builddir)/ssh --basedir=$(dir $<) $^
+endif
+$(ssh_C): $(ssh_HEADER)
+
+ssh_BUILT = \
+ $(ssh_C) \
+ $(ssh_HEADER)
+
+BUILT_SOURCES += $(ssh_BUILT)
+
+EXTRA_DIST += \
+ $(ssh_BUILT) \
+ $(ssh_VALA) \
+ $(NULL)
+
+ui_files += \
+ ssh/seahorse-ssh-key-properties.ui \
+ ssh/seahorse-ssh-generate.ui \
+ ssh/seahorse-ssh-upload.ui
+
+# Askpass binary
seahorselibexecbin_PROGRAMS += seahorse-ssh-askpass
seahorse_ssh_askpass_SOURCES = \
@@ -24,9 +63,4 @@ seahorse_ssh_askpass_SOURCES = \
seahorse_ssh_askpass_LDADD = \
$(SEAHORSE_LIBS)
-ui_files += \
- ssh/seahorse-ssh-key-properties.ui \
- ssh/seahorse-ssh-generate.ui \
- ssh/seahorse-ssh-upload.ui
-
-include $(top_srcdir)/git.mk
diff --git a/ssh/actions.vala b/ssh/actions.vala
new file mode 100644
index 0000000..71770d6
--- /dev/null
+++ b/ssh/actions.vala
@@ -0,0 +1,78 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2008 Stefan Walter
+ * Copyright (C) 2011 Collabora Ltd.
+ * Copyright (C) 2016 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+public class Seahorse.Ssh.Actions : Seahorse.Actions {
+
+ private const Gtk.ActionEntry KEYS_ACTIONS[] = {
+ { "remote-ssh-upload", null, N_("Configure Key for _Secure Shell…"), null,
+ N_("Send public Secure Shell key to another machine, and enable logins using that key."),
+ on_ssh_upload }
+ };
+
+ public const string UI_DEFINITION = """
+ <ui>
+ <menubar>
+ <placeholder name="RemoteMenu">
+ <menu name="Remote" action="remote-menu">
+ <menuitem action="remote-ssh-upload"/>
+ </menu>
+ </placeholder>
+ </menubar>
+ <popup name="ObjectPopup">
+ <menuitem action="remote-ssh-upload"/>
+ </popup>
+ </ui>""";
+
+ construct {
+ set_translation_domain(Config.GETTEXT_PACKAGE);
+ add_actions(KEYS_ACTIONS, this);
+ register_definition(UI_DEFINITION);
+ }
+
+ private Actions(string name) {
+ base(name);
+ }
+
+ private static Actions _instance = null;
+
+ public static unowned Actions instance() {
+ if (_instance == null) {
+ _instance = new Actions("SshKey");
+ }
+
+ return _instance;
+ }
+
+ private void on_ssh_upload (Gtk.Action action) {
+ List<Key> keys = new List<Key>();
+
+ if (this.catalog != null) {
+ foreach (GLib.Object el in this.catalog.get_selected_objects()) {
+ Key key = el as Key;
+ if (key != null)
+ keys.prepend(key);
+ }
+ }
+
+ Upload.prompt(keys, Seahorse.Action.get_window(action));
+ }
+}
diff --git a/ssh/algorithm.vala b/ssh/algorithm.vala
new file mode 100644
index 0000000..c300d91
--- /dev/null
+++ b/ssh/algorithm.vala
@@ -0,0 +1,87 @@
+/*
+ * Seahorse
+ *
+ * Copyright (c) 2016 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Enumerates the possible algorithms in which an SSH key can be encrypted.
+ * This is also known as the 'type' of a key.
+ */
+public enum Seahorse.Ssh.Algorithm {
+ UNKNOWN,
+ RSA,
+ DSA;
+
+ /**
+ * Returns a (non-localized) string representation.
+ *
+ * @return A string representation, or null if UNKNOWN.
+ */
+ public string? to_string() {
+ switch(this) {
+ case UNKNOWN:
+ return "";
+ case RSA:
+ return "RSA";
+ case DSA:
+ return "DSA";
+ default:
+ assert_not_reached ();
+ };
+ }
+
+ /**
+ * Converts a string representation into a Algorithm.
+ *
+ * @return The algorithm (can be UNKNOWN).
+ */
+ public static Algorithm from_string(string? type) {
+ if (type == null || type == "")
+ return UNKNOWN;
+
+ string _type = type.strip().down();
+ switch (_type) {
+ case "rsa":
+ return RSA;
+ case "dsa":
+ case "dss":
+ return DSA;
+ default:
+ return UNKNOWN;
+ }
+ }
+
+ /**
+ * Tries to make an educated guess for the algorithm type.
+ * Basically it just checks if the given string contains the type.
+ *
+ * @return The guessed algorithm (can be UNKNOWN)
+ */
+ public static Algorithm guess_from_string(string? str) {
+ if (str == null || str == "")
+ return UNKNOWN;
+
+ string str_down = str.down();
+ if ("rsa" in str_down)
+ return RSA;
+
+ if (("dsa" in str_down) || ("dss" in str_down))
+ return DSA;
+
+ return UNKNOWN;
+ }
+}
diff --git a/ssh/backend.vala b/ssh/backend.vala
new file mode 100644
index 0000000..9380f0c
--- /dev/null
+++ b/ssh/backend.vala
@@ -0,0 +1,84 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2008 Stefan Walter
+ * Copyright (C) 2016 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+public class Seahorse.Ssh.Backend : GLib.Object, Gcr.Collection, Seahorse.Backend {
+
+ private Source dot_ssh;
+
+ public string name { get { return SEAHORSE_SSH_NAME; } }
+ public string label { get { return _("Secure Shell"); } }
+ public string description { get { return _("Keys used to connect securely to other computers"); } }
+ public Gtk.ActionGroup? actions { owned get { return null; } }
+
+ private bool _loaded;
+ public bool loaded { get { return _loaded; } }
+
+ construct {
+ this.dot_ssh = new Source();
+ this.dot_ssh.load.begin(null, (obj, res) => {
+ try {
+ this._loaded = dot_ssh.load.end(res);
+ } catch (GLib.Error e) {
+ warning("Failed to initialize SSH backend: %s", e.message);
+ }
+ });
+ }
+
+ public Backend() {
+ register();
+ }
+
+ public static Backend? instance { get; internal set; default = null; }
+
+ public uint get_length() {
+ return 1;
+ }
+
+ public List<weak GLib.Object> get_objects() {
+ List<GLib.Object> list = new List<GLib.Object>();
+ list.append(this.dot_ssh);
+ return list;
+ }
+
+ public bool contains(GLib.Object object) {
+ Source? src = object as Source;
+
+ return (src != null) && (this.dot_ssh == src);
+ }
+
+ public Seahorse.Place? lookup_place(string uri) {
+ if (this.dot_ssh != null && this.dot_ssh.uri != null && this.dot_ssh.uri == uri)
+ return this.dot_ssh;
+
+ return null;
+ }
+
+ public static void initialize() {
+ if (Config.WITH_SSH) {
+ instance = new Backend();
+ Generate.register();
+ }
+ }
+
+ public Source get_dot_ssh() {
+ return this.dot_ssh;
+ }
+}
diff --git a/ssh/deleter.vala b/ssh/deleter.vala
new file mode 100644
index 0000000..22c5e1b
--- /dev/null
+++ b/ssh/deleter.vala
@@ -0,0 +1,123 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2011 Collabora Ltd.
+ * Copyright (C) 2016 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Author: Stef Walter <stefw collabora co uk>
+ */
+
+public class Seahorse.Ssh.Deleter : Seahorse.Deleter {
+
+ private bool have_private;
+ private List<Key> keys;
+
+ public Deleter(Key key) {
+ if (!add_object(key))
+ assert_not_reached ();
+ }
+
+ public override Gtk.Dialog create_confirm(Gtk.Window? parent) {
+ uint num = this.keys.length();
+ assert(num != 1);
+
+ string confirm, prompt;
+ if (this.have_private) {
+
+ prompt = _("Are you sure you want to delete the secure shell key “%s”?")
+ .printf(this.keys.data.label);
+ confirm = _("I understand that this secret key will be permanently deleted.");
+
+ } else if (num == 1) {
+ prompt = _("Are you sure you want to delete the secure shell key “%s”?")
+ .printf(this.keys.data.label);
+ confirm = null;
+
+ } else {
+ prompt = ngettext("Are you sure you want to delete %u secure shell key?",
+ "Are you sure you want to delete %u secure shell keys?",
+ num).printf(num);
+ confirm = null;
+ }
+
+ Seahorse.DeleteDialog dialog = new Seahorse.DeleteDialog(parent, "%s", prompt);
+
+ if (confirm != null) {
+ dialog.check_label = confirm;
+ dialog.check_require = true;
+ }
+
+ return dialog;
+ }
+
+ public override unowned List<weak GLib.Object> get_objects () {
+ return this.keys;
+ }
+
+ public override bool add_object(GLib.Object object) {
+ Key key = object as Key;
+ if (this.have_private || key == null)
+ return false;
+
+ if (key.usage == Seahorse.Usage.PRIVATE_KEY) {
+ if (this.keys != null)
+ return false;
+ this.have_private = true;
+ }
+
+ this.keys.append(key);
+ return true;
+ }
+
+ public override async bool delete(GLib.Cancellable? cancellable) throws GLib.Error {
+ foreach (Key key in this.keys)
+ delete_key(key);
+
+ return true;
+ }
+
+ /**
+ * Deletes a given key.
+ *
+ * @param key The key that should be deleted.
+ */
+ public void delete_key(Key key) throws GLib.Error {
+ KeyData? keydata = key.key_data;
+ if (keydata == null)
+ throw new Error.GENERAL("Can't delete key with empty KeyData.");
+
+ if (keydata.partial) { // Just part of a file for this key
+ if (keydata.pubfile != null) // Take just that line out of the file
+ KeyData.filter_file(keydata.pubfile, null, keydata);
+
+ } else { // A full file for this key
+ if (keydata.pubfile != null) {
+ if (FileUtils.unlink(keydata.pubfile) == -1)
+ throw new Error.GENERAL("Couldn't delete public key file '%s'".printf(keydata.pubfile));
+ }
+
+ if (keydata.privfile != null) {
+ if (FileUtils.unlink(keydata.privfile) == -1) {
+ throw new Error.GENERAL("Couldn't delete private key file
'%s'".printf(keydata.privfile));
+ }
+ }
+ }
+
+ Source source = (Source) key.place;
+ source.remove_object(key);
+ }
+}
diff --git a/ssh/errors.vala b/ssh/errors.vala
new file mode 100644
index 0000000..5f3301c
--- /dev/null
+++ b/ssh/errors.vala
@@ -0,0 +1,21 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2016 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+public errordomain Seahorse.Ssh.Error {
+ GENERAL
+}
diff --git a/ssh/exporter.vala b/ssh/exporter.vala
new file mode 100644
index 0000000..f2d70b4
--- /dev/null
+++ b/ssh/exporter.vala
@@ -0,0 +1,130 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2011 Collabora Ltd.
+ * Copyright (C) 2016 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Author: Stef Walter <stefw collabora co uk>
+ */
+
+public class Seahorse.Ssh.Exporter : GLib.Object, Seahorse.Exporter {
+
+ private Key? key;
+ private List<Key> objects;
+
+ private bool _secret;
+ public bool secret {
+ get { return this._secret; }
+ set {
+ this._secret = value;
+ notify_property("filename");
+ notify_property("file-filter");
+ notify_property("content-type");
+ }
+ }
+
+ public string filename { owned get { return get_filename(); } }
+
+ public string content_type {
+ get {
+ return this.secret ? "application/x-pem-key" : "application/x-ssh-key";
+ }
+ }
+
+ public Gtk.FileFilter file_filter {
+ owned get {
+ Gtk.FileFilter filter = new Gtk.FileFilter();
+
+ if (this.secret) {
+ filter.set_name(_("Secret SSH keys"));
+ filter.add_mime_type("application/x-pem-key");
+ filter.add_pattern("id_*");
+ } else {
+ filter.set_name(_("Public SSH keys"));
+ filter.add_mime_type("application/x-ssh-key");
+ filter.add_pattern("*.pub");
+ }
+
+ return filter;
+ }
+ }
+
+ public Exporter(Key key, bool secret) {
+ this.secret = secret;
+
+ if (!add_object(key))
+ assert_not_reached ();
+ }
+
+ private string? get_filename() {
+ if (this.key == null)
+ return null;
+
+ KeyData? data = this.key.key_data;
+ if (data != null && !data.partial) {
+ string? location = null;
+ if (this.secret && data.privfile != null)
+ location = data.privfile;
+ else if (!this.secret && data.pubfile != null)
+ location = data.pubfile;
+ if (location != null)
+ return Path.get_basename(location);
+ }
+
+ string basename = this.key.nickname ?? _("SSH Key");
+
+ if (this.secret) {
+ string filename = "id_%s".printf(basename).strip();
+ filename.delimit(BAD_FILENAME_CHARS, '_');
+ return filename;
+ } else {
+ string filename = "%s.pub".printf(basename).strip();
+ filename.delimit(BAD_FILENAME_CHARS, '_');
+ return filename;
+ }
+ }
+
+ public unowned GLib.List<weak GLib.Object> get_objects() {
+ return this.objects;
+ }
+
+ public bool add_object(GLib.Object object) {
+ Key key = object as Key;
+ if (key == null)
+ return false;
+
+ if (this.secret && key.usage != Seahorse.Usage.PRIVATE_KEY)
+ return false;
+
+ this.key = key;
+ this.objects.append(this.key);
+ notify_property("filename");
+ return true;
+ }
+
+ public async uint8[] export(GLib.Cancellable? cancellable) throws GLib.Error {
+ KeyData keydata = this.key.key_data;
+
+ if (this.secret)
+ return ((Source) this.key.place).export_private(this.key).data;
+
+ if (keydata.pubfile == null)
+ throw new Error.GENERAL(_("No public key file is available for this key."));
+
+ assert(keydata.rawdata != null);
+ return "%s\n".printf(keydata.rawdata).data;
+ }
+}
diff --git a/ssh/generate.vala b/ssh/generate.vala
new file mode 100644
index 0000000..6cb26e8
--- /dev/null
+++ b/ssh/generate.vala
@@ -0,0 +1,155 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2006 Stefan Walter
+ * Copyright (C) 2011 Collabora Ltd.
+ * Copyright (C) Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+public class Seahorse.Ssh.Generate : Gtk.Dialog {
+ public const int DEFAULT_DSA_SIZE = 1024;
+ public const int DEFAULT_RSA_SIZE = 2048;
+
+ private const Gtk.ActionEntry ACTION_ENTRIES[] = {
+ { "ssh-generate-key", Gcr.ICON_KEY_PAIR, N_ ("Secure Shell Key"), "",
+ N_("Used to access other computers (eg: via a terminal)"), on_ssh_generate_key }
+ };
+
+ private Source source;
+
+ private Gtk.SpinButton bits_spin_button;
+ private Gtk.Entry email_entry;
+ private Gtk.ComboBoxText algorithm_combo_box;
+ private Gtk.Button create_with_setup_button;
+ private Gtk.Button create_no_setup_button;
+
+ public Generate(Source src, Gtk.Window parent) {
+ GLib.Object(border_width: 5,
+ title: _("New Secure Shell Key"),
+ resizable: false,
+ modal: true,
+ transient_for: parent);
+ this.source = src;
+
+ load_ui();
+
+ // on_change() gets called, bits entry is setup
+ algorithm_combo_box.set_active(0);
+ }
+
+ private static void on_ssh_generate_key(Gtk.Action action) {
+ Generate generate_dialog = new Generate(Backend.instance.get_dot_ssh(),
+ Action.get_window(action));
+ generate_dialog.show();
+ }
+
+ public static void register() {
+ Gtk.ActionGroup actions = new Gtk.ActionGroup("ssh-generate");
+
+ actions.set_translation_domain(Config.GETTEXT_PACKAGE);
+ actions.add_actions(ACTION_ENTRIES, null);
+
+ // Register this as a generator
+ Seahorse.Registry.register_object(actions, "generator");
+ }
+
+ // FIXME: normally we would do this using GtkTemplate, but this is quite hard with the current build
setup
+ private void load_ui() {
+ Gtk.Builder builder = new Gtk.Builder();
+ try {
+ string path = "/org/gnome/Seahorse/seahorse-ssh-generate.ui";
+ builder.add_from_resource(path);
+ } catch (GLib.Error err) {
+ GLib.critical("%s", err.message);
+ }
+ Gtk.Container content = (Gtk.Container) builder.get_object("ssh-generate");
+ ((Gtk.Container)this.get_content_area()).add(content);
+ Gtk.Widget actions = (Gtk.Widget) builder.get_object("action_area");
+ ((Gtk.Container)this.get_action_area()).add(actions);
+
+ this.bits_spin_button = (Gtk.SpinButton) builder.get_object("bits-spin-button");
+ this.email_entry = (Gtk.Entry) builder.get_object("email-entry");
+ this.algorithm_combo_box = (Gtk.ComboBoxText) builder.get_object("algorithm-combo-box");
+ this.create_no_setup_button = (Gtk.Button) builder.get_object("create-no-setup");
+ this.create_with_setup_button = (Gtk.Button) builder.get_object("create-with-setup");
+
+ // Signals
+ this.algorithm_combo_box.changed.connect(on_change);
+ this.create_no_setup_button.clicked.connect((b) => create_key(false));
+ this.create_with_setup_button.clicked.connect((b) => create_key(true));
+ }
+
+ private void on_change(Gtk.ComboBox combo) {
+ string t = algorithm_combo_box.get_active_text();
+ if (Algorithm.from_string(t) == Algorithm.DSA) {
+ this.bits_spin_button.set_value(DEFAULT_DSA_SIZE);
+ this.bits_spin_button.sensitive = false;
+ } else {
+ this.bits_spin_button.set_value(DEFAULT_RSA_SIZE);
+ this.bits_spin_button.sensitive = true;
+ }
+ }
+
+ private void create_key(bool upload) {
+ // The email address
+ string email = this.email_entry.text;
+
+ // The algorithm
+ string t = this.algorithm_combo_box.get_active_text();
+ Algorithm type = Algorithm.from_string(t);
+ assert(type != Algorithm.UNKNOWN);
+
+ // The number of bits
+ uint bits = this.bits_spin_button.get_value_as_int();
+ if (bits < 512 || bits > 8192) {
+ message("Invalid key size: %s defaulting to 2048", t);
+ bits = 2048;
+ }
+
+ // The filename
+ string filename = this.source.new_filename_for_algorithm(type);
+
+ // We start creation
+ Cancellable cancellable = new Cancellable();
+ GenerateOperation op = new GenerateOperation();
+ op.generate_async.begin(filename, email, type, bits, cancellable, (obj, res) => {
+ try {
+ op.generate_async.end(res);
+
+ // The result of the operation is the key we generated
+ source.add_key_from_filename.begin(filename, (obj, res) => {
+ try {
+ Key key = source.add_key_from_filename.end(res);
+
+ if (upload && key != null) {
+ List<Key> keys = new List<Key>();
+ keys.append(key);
+ Upload.prompt(keys, null);
+ }
+ } catch (GLib.Error e) {
+ Seahorse.Util.show_error(null, _("Couldn’t load newly generated Secure Shell key"),
e.message);
+ }
+ });
+ } catch (GLib.Error e) {
+ Seahorse.Util.show_error(null, _("Couldn’t generate Secure Shell key"), e.message);
+ }
+ });
+ Seahorse.Progress.show(cancellable, _("Creating Secure Shell Key"), false);
+
+ destroy();
+ }
+
+}
diff --git a/ssh/key-data.vala b/ssh/key-data.vala
new file mode 100644
index 0000000..d26621b
--- /dev/null
+++ b/ssh/key-data.vala
@@ -0,0 +1,255 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2006 Stefan Walter
+ * Copyright (C) 2016 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+public class Seahorse.Ssh.KeyData : GLib.Object {
+
+ /* Used by callers */
+ public string privfile { get; set; } /* The secret key file */
+ public string pubfile { get; set; } /* The public key file */
+ public bool partial { get; set; } /* Only part of the public key file */
+ public bool authorized { get; set; default = false; } /* Is in authorized_keys */
+
+ // These props are filled in by the parser
+ public string rawdata { get; internal set; } /* The raw data of the public key */
+ public string? comment { get; internal set; } /* The comment for the public key */
+ public string? fingerprint { get; internal set; } /* The full fingerprint hash */
+ public uint length { get; internal set; } /* Number of bits */
+ public Algorithm algo { get; internal set; } /* Key algorithm */
+
+ public bool is_valid() {
+ return this.fingerprint != null;
+ }
+
+ /**
+ * Checks whether this key and another (in the form of a string) match.
+ */
+ public bool match(string line) {
+ if (!is_valid())
+ return false;
+
+ try {
+ KeyData other = parse_line(line);
+ return (other.fingerprint != null) && (this.fingerprint == other.fingerprint);
+ } catch (GLib.Error e) {
+ warning(e.message);
+ }
+
+ return false;
+ }
+
+ public static KeyData parse_line(string? line) throws GLib.Error {
+ if (line == null || line.strip() == "")
+ throw new Error.GENERAL("Can't parse key from empty line.");
+
+ string no_leading = line.chug();
+ KeyData result = new KeyData();
+ result.rawdata = no_leading;
+
+ // Get the type
+ string[] type_rest = no_leading.split(" ", 2);
+ if (type_rest.length != 2)
+ throw new Error.GENERAL("Can't distinguish type from data (space missing).");
+
+ string type = type_rest[0];
+ if (type == "")
+ throw new Error.GENERAL("Key doesn't have a type.");
+
+ result.algo = Algorithm.guess_from_string(type);
+ if (result.algo == Algorithm.UNKNOWN)
+ throw new Error.GENERAL("Key doesn't have a valid type.");
+
+ // Prepare for decoding
+ string rest = type_rest[1];
+ if (rest == "")
+ throw new Error.GENERAL("Key doesn't have any data.");
+ string[] data_comment = rest.split(" ", 2);
+
+ // Decode it, and parse binary stuff
+ uchar[] bytes = Base64.decode(data_comment[0].strip());
+ result.fingerprint = parse_key_blob(bytes);
+
+ // The number of bits
+ result.length = calc_bits(result.algo, bytes.length);
+
+ // And the rest is the comment
+ if (data_comment.length == 2) {
+ string comment = data_comment[1];
+
+ if (!comment.validate()) // If not utf8-valid, assume latin1
+ result.comment = convert(comment, comment.length, "UTF-8", "ISO-8859-1");
+ else
+ result.comment = comment;
+ }
+
+ return result;
+ }
+
+ internal static string parse_key_blob (uchar[] bytes) throws GLib.Error {
+ string digest = Checksum.compute_for_data(ChecksumType.MD5, bytes);
+ if (digest == null)
+ throw new Error.GENERAL("Can't calculate fingerprint from key.");
+
+ StringBuilder fingerprint = new StringBuilder.sized((digest.length * 3) / 2);
+ for (size_t i = 0; i < digest.length; i += 2) {
+ if (i > 0)
+ fingerprint.append_c(':');
+ fingerprint.append(digest.substring((long) i, 2));
+ }
+
+ return fingerprint.str;
+ }
+
+ internal static uint calc_bits (Algorithm algo, uint len) {
+ // To keep us from having to parse a BIGNUM and link to openssl, these
+ // are from the hip guesses at the bits of a key based on the size of
+ // the binary blob in the public key.
+ switch (algo) {
+ case Algorithm.RSA:
+ // Seems accurate to nearest 8 bits
+ return ((len - 21) * 8);
+
+ case Algorithm.DSA:
+ // DSA keys seem to only work at 'bits % 64 == 0' boundaries
+ uint n = ((len - 50) * 8) / 3;
+ return ((n / 64) + (((n % 64) > 32) ? 1 : 0)) * 64; // round to 64
+
+ default:
+ return 0;
+ }
+ }
+
+ // Adds and/or removes a keydata to a file (if added is already there, it is added at the back of the
file).
+ public static void filter_file(string filename, KeyData? add, KeyData? remove = null) throws GLib.Error {
+ // By default filter out the one we're adding
+ if (remove == null)
+ remove = add;
+
+ string contents;
+ FileUtils.get_contents(filename, out contents);
+
+ StringBuilder results = new StringBuilder();
+
+ // Load each line
+ bool first = true;
+ string[] lines = (contents ?? "").split("\n");
+ foreach (string line in lines) {
+ if (remove != null && remove.match(line))
+ continue;
+
+ if (!first)
+ results.append_c('\n');
+ first = false;
+ results.append(line);
+ }
+
+ // Add any that need adding
+ if (add != null) {
+ if (!first)
+ results.append_c('\n');
+ results.append(add.rawdata);
+ }
+
+ FileUtils.set_contents(filename, results.str);
+ }
+
+ public string? get_location() {
+ return this.privfile != null ? this.privfile : this.pubfile;
+ }
+}
+
+/**
+ * Represents the data in a private key.
+ */
+public class Seahorse.Ssh.SecData : GLib.Object {
+ public const string SSH_KEY_SECRET_SIG = "# SSH PRIVATE KEY: ";
+ public const string SSH_PRIVATE_BEGIN = "-----BEGIN ";
+ public const string SSH_PRIVATE_END = "-----END ";
+
+ /**
+ * Everything excluding the comment
+ */
+ public string rawdata { get; internal set; }
+
+ public string? comment { get; internal set; }
+
+ public Algorithm algo { get; internal set; }
+
+ public static bool contains_private_key(string data) {
+ return (SSH_KEY_SECRET_SIG in data) || (SSH_PRIVATE_BEGIN in data);
+ }
+
+ /**
+ * Finds the first occurence of a private key in the given string and parses it if found.
+ * NOTE: after parsing, it will *remove* the data with the private key from the string.
+ *
+ * @param data The data that contains a private key.
+ */
+ public static SecData parse_data(StringBuilder data) throws GLib.Error {
+ SecData secdata = new SecData();
+
+ // Get the comment
+ if (data.str.has_prefix(SSH_KEY_SECRET_SIG)) {
+ string comment = data.str.split("\n", 2)[0];
+ secdata.comment = comment.substring(SSH_KEY_SECRET_SIG.length).strip();
+ }
+
+ // First get our raw data (if there is none, don't bother)
+ string rawdata = parse_lines_block(data, SSH_PRIVATE_BEGIN, SSH_PRIVATE_END);
+ if (rawdata == null || rawdata == "")
+ throw new Error.GENERAL("Private key contains no data.");
+
+ secdata.rawdata = rawdata;
+
+ // Guess at the algorithm type
+ secdata.algo = Algorithm.guess_from_string(rawdata);
+
+ return secdata;
+ }
+
+ /**
+ * Takes everything between the start and end pattern and returns it.
+ * NOTE: The string (if found will) be removed from the argument.
+ */
+ private static string parse_lines_block(StringBuilder data, string start, string end) {
+ StringBuilder result = new StringBuilder();
+
+ bool start_found = false;
+ string[] lines = data.str.split("\n");
+ foreach (string line in lines) {
+ // Look for the beginning
+ if (!start_found) {
+ if (start in line) {
+ result.append_printf("%s\n", line);
+ result.erase(0, line.length + 1);
+ start_found = true;
+ continue;
+ }
+ } else {
+ // Look for the end
+ result.append_printf("%s\n", line);
+ result.erase(0, line.length + 1);
+ if (end in line)
+ break;
+ }
+ }
+
+ return result.str;
+ }
+}
diff --git a/ssh/key-properties.vala b/ssh/key-properties.vala
new file mode 100644
index 0000000..4632cf2
--- /dev/null
+++ b/ssh/key-properties.vala
@@ -0,0 +1,198 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2005 Stefan Walter
+ * Copyright (C) 2011 Collabora Ltd.
+ * Copyright (C) 2016 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+public class Seahorse.Ssh.KeyProperties : Gtk.Dialog {
+
+ private Key key;
+
+ // Used to make sure we don't start calling command unnecessarily
+ private bool updating_ui = false;
+
+ private Gtk.Image key_image;
+ private Gtk.Entry comment_entry;
+ private Gtk.Label id_label;
+ private Gtk.Label trust_message;
+ private Gtk.ToggleButton trust_check;
+
+ private Gtk.Button passphrase_button;
+ private Gtk.Button export_button;
+
+ private Gtk.Label fingerprint_label;
+ private Gtk.Label algo_label;
+ private Gtk.Label location_label;
+ private Gtk.Label strength_label;
+
+ public KeyProperties(Key key, Gtk.Window parent) {
+ GLib.Object(border_width: 5,
+ title: _("Key Properties"),
+ resizable: false,
+ transient_for: parent);
+ this.key = key;
+
+ load_ui();
+ update_ui();
+
+ // A public key only
+ if (key.usage != Seahorse.Usage.PRIVATE_KEY) {
+ passphrase_button.visible = false;
+ export_button.visible = false;
+ }
+
+ this.key.notify.connect(() => update_ui());
+ }
+
+ // FIXME: normally we would do this using GtkTemplate, but this is quite hard with the current build
setup
+ private void load_ui() {
+ Gtk.Builder builder = new Gtk.Builder();
+ try {
+ string path = "/org/gnome/Seahorse/seahorse-ssh-key-properties.ui";
+ builder.add_from_resource(path);
+ } catch (GLib.Error err) {
+ GLib.critical("%s", err.message);
+ }
+ Gtk.Container content = (Gtk.Container) builder.get_object("ssh-key-properties");
+ ((Gtk.Container)this.get_content_area()).add(content);
+
+ this.key_image = (Gtk.Image) builder.get_object("key-image");
+ this.comment_entry = (Gtk.Entry) builder.get_object("comment-entry");
+ this.id_label = (Gtk.Label) builder.get_object("id-label");
+ this.trust_message = (Gtk.Label) builder.get_object("trust-message");
+ this.trust_check = (Gtk.ToggleButton) builder.get_object("trust-check");
+ this.passphrase_button = (Gtk.Button) builder.get_object("passphrase-button");
+ this.export_button = (Gtk.Button) builder.get_object("export-button");
+ this.fingerprint_label = (Gtk.Label) builder.get_object("fingerprint-label");
+ this.algo_label = (Gtk.Label) builder.get_object("algo-label");
+ this.location_label = (Gtk.Label) builder.get_object("location-label");
+ this.strength_label = (Gtk.Label) builder.get_object("strength-label");
+
+ // Signals
+ this.comment_entry.activate.connect(on_ssh_comment_activate);
+ this.comment_entry.focus_out_event.connect(on_ssh_comment_focus_out);
+ this.trust_check.toggled.connect(on_ssh_trust_toggled);
+ this.passphrase_button.clicked.connect(on_ssh_passphrase_button_clicked);
+ this.export_button.clicked.connect(on_ssh_export_button_clicked);
+ }
+
+ private void update_ui() {
+ this.updating_ui = true;
+
+ // Image
+ this.key_image.set_from_icon_name(Seahorse.ICON_KEY_SSH, Gtk.IconSize.DIALOG);
+ // Name and title
+ this.comment_entry.text = this.key.label;
+ this.title = this.key.label;
+ // Key id
+ this.id_label.label = this.key.identifier;
+ // Put in message
+ string template = this.trust_message.label;
+ this.trust_message.set_markup(template.printf(Environment.get_user_name()));
+
+ // Setup the check
+ this.trust_check.active = (this.key.trust >= Seahorse.Validity.FULL);
+
+ this.fingerprint_label.label = this.key.fingerprint;
+ this.algo_label.label = this.key.get_algo().to_string() ?? _("Unknown type");
+ this.location_label.label = this.key.get_location();
+ this.strength_label.label = "%u".printf(this.key.get_strength());
+
+ this.updating_ui = false;
+ }
+
+ public override void response(int response) {
+ destroy();
+ }
+
+ public void on_ssh_comment_activate(Gtk.Entry entry) {
+ // Make sure not the same
+ if (key.key_data.comment != null && entry.text == key.key_data.comment)
+ return;
+
+ entry.sensitive = false;
+
+ RenameOperation op = new RenameOperation();
+ op.rename_async.begin(key, entry.text, this, (obj, res) => {
+ try {
+ op.rename_async.end(res);
+ } catch (GLib.Error e) {
+ Seahorse.Util.show_error(this, _("Couldn’t rename key."), e.message);
+ entry.text = key.key_data.comment ?? "";
+ }
+
+ entry.sensitive = true;
+ });
+ }
+
+ private bool on_ssh_comment_focus_out(Gtk.Widget widget, Gdk.EventFocus event) {
+ on_ssh_comment_activate((Gtk.Entry) widget);
+ return false;
+ }
+
+ private void on_ssh_trust_toggled(Gtk.ToggleButton button) {
+ if (updating_ui)
+ return;
+
+ button.sensitive = false;
+
+ Source source = (Source) key.place;
+ source.authorize_async.begin(key, button.active, (obj, res) => {
+ try {
+ source.authorize_async.end(res);
+ } catch (GLib.Error e) {
+ Seahorse.Util.show_error(this, _("Couldn’t change authorization for key."), e.message);
+ }
+
+ button.sensitive = true;
+ });
+ }
+
+ private void on_ssh_passphrase_button_clicked (Gtk.Button button) {
+ button.sensitive = false;
+
+ ChangePassphraseOperation op = new ChangePassphraseOperation();
+ op.change_passphrase_async.begin(key, null, (obj, res) => {
+ try {
+ op.change_passphrase_async.end(res);
+ } catch (GLib.Error e) {
+ Seahorse.Util.show_error(this, _("Couldn't change passphrase for key."), e.message);
+ }
+
+ button.sensitive = true;
+ });
+ }
+
+ private void on_ssh_export_button_clicked (Gtk.Widget widget) {
+ List<Exporter> exporters = new List<Exporter>();
+ exporters.append(new Exporter(key, true));
+
+ Seahorse.Exporter exporter;
+ string directory = null;
+ File file;
+ if (Seahorse.Exportable.prompt(exporters, this, ref directory, out file, out exporter)) {
+ exporter.export_to_file.begin(file, true, null, (obj, res) => {
+ try {
+ exporter.export_to_file.end(res);
+ } catch (GLib.Error e) {
+ Seahorse.Util.show_error(this, _("Couldn’t export key"), e.message);
+ }
+ });
+ }
+ }
+}
diff --git a/ssh/key.vala b/ssh/key.vala
new file mode 100644
index 0000000..2a49854
--- /dev/null
+++ b/ssh/key.vala
@@ -0,0 +1,340 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2005 Stefan Walter
+ * Copyright (C) 2011 Collabora Ltd.
+ * Copyright (C) 2016 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Represents an SSH key, consisting of a public/private pair.
+ */
+public class Seahorse.Ssh.Key : Seahorse.Object, Seahorse.Exportable, Seahorse.Deletable, Seahorse.Viewable {
+ public const int SSH_IDENTIFIER_SIZE = 8;
+
+ private KeyData? _keydata;
+ public KeyData? key_data {
+ get { return _keydata; }
+ set { this._keydata = value; changed_key(); }
+ }
+
+ /**
+ * Unique fingerprint for this key
+ */
+ public string? fingerprint {
+ get { return (this.key_data != null) ? this.key_data.fingerprint : null; }
+ }
+
+ /**
+ * Description
+ */
+ public string description {
+ get { return this.usage == Seahorse.Usage.PRIVATE_KEY ? _("Personal SSH key") : _("SSH key"); }
+ }
+
+ /**
+ * Validity of this key
+ */
+ public Seahorse.Validity validity {
+ get {
+ if (this.key_data != null && this.key_data.privfile != null)
+ return Seahorse.Validity.ULTIMATE;
+ return 0;
+ }
+ }
+
+ /**
+ * Trust in this key
+ */
+ public Seahorse.Validity trust {
+ get {
+ if (this.key_data != null && this.key_data.authorized)
+ return Seahorse.Validity.FULL;
+ return 0;
+ }
+ }
+
+ /**
+ * Date this key expires on (0 if never)
+ */
+ public ulong expires {
+ get { return 0; }
+ }
+
+ /**
+ * The length of this key
+ */
+ public uint length {
+ get { return this.key_data != null ? this.key_data.length : 0; }
+ }
+
+ public bool deletable {
+ get { return Seahorse.Flags.DELETABLE in this.flags; }
+ }
+
+ public bool exportable {
+ get { return Seahorse.Flags.EXPORTABLE in this.flags; }
+ }
+
+ public Key(Source source, KeyData key_data) {
+ GLib.Object(place: source, key_data: key_data);
+ }
+
+ private string? parse_first_word(string line) {
+ string PARSE_CHARS = "\t \n@;,.\\?()[]{}+/";
+
+ string[] words = line.split_set(PARSE_CHARS, 2);
+ return (words.length == 2)? words[0] : null;
+ }
+
+ private void changed_key() {
+ string display = null;
+ string simple = null;
+
+ // TODO - this is a stupid hack until we can set the properties normally
+ Value labelValue = Value(typeof(string));
+ Value actionsValue = Value(typeof(Gtk.ActionGroup));
+ Value iconValue = Value(typeof(Icon?));
+ Value usageValue = Value(typeof(Seahorse.Usage));
+ Value nicknameValue = Value(typeof(string));
+ Value flagsValue = Value(typeof(uint));
+ Value markupValue = Value(typeof(string));
+ Value identifierValue = Value(typeof(string));
+
+ if (this.key_data != null) {
+ // Try to make display and simple names
+ if (this.key_data.comment != null) {
+ display = this.key_data.comment;
+ simple = parse_first_word(this.key_data.comment);
+
+ // No names when not even the fingerpint loaded
+ } else if (this.key_data.fingerprint == null) {
+ display = _("(Unreadable Secure Shell Key)");
+ // No comment, but loaded
+ } else {
+ display = _("Secure Shell Key");
+ }
+
+ if (simple == null)
+ simple = _("Secure Shell Key");
+ }
+
+ if (this.key_data == null || this.key_data.fingerprint == null) {
+ labelValue.set_string("");
+ set_property("label", labelValue);
+ iconValue.set_object(null);
+ set_property("icon", iconValue);
+ usageValue.set_enum(Seahorse.Usage.NONE);
+ set_property("usage", usageValue);
+ labelValue.set_string("");
+ set_property("nickname", nicknameValue);
+ usageValue.set_uint((uint) Seahorse.Flags.DISABLED);
+ set_property("object-flags", flagsValue);
+ return;
+ }
+
+ Seahorse.Usage usage;
+ Seahorse.Flags flags = Seahorse.Flags.EXPORTABLE | Seahorse.Flags.DELETABLE;
+ Icon icon;
+ if (this.key_data.privfile != null) {
+ usage = Seahorse.Usage.PRIVATE_KEY;
+ flags |= Seahorse.Flags.PERSONAL | Seahorse.Flags.TRUSTED;
+ icon = new ThemedIcon(Gcr.ICON_KEY_PAIR);
+ } else {
+ flags = 0;
+ usage = Seahorse.Usage.PUBLIC_KEY;
+ icon = new ThemedIcon(Gcr.ICON_KEY);
+ }
+
+ string filename = Path.get_basename(this.key_data.privfile ?? this.key_data.pubfile);
+
+ string markup = Markup.printf_escaped("%s<span size='small' rise='0'
foreground='#555555'>\n%s</span>",
+ display, filename);
+
+ string identifier = calc_identifier(this.key_data.fingerprint);
+ Actions actions = Actions.instance();
+
+ if (this.key_data.authorized)
+ flags |= Seahorse.Flags.TRUSTED;
+
+ actionsValue.set_object(actions);
+ set_property("actions", actionsValue);
+ markupValue.set_string(markup);
+ set_property("markup", markupValue);
+ labelValue.set_string(display);
+ set_property("label", labelValue);
+ iconValue.set_object(icon);
+ set_property("icon", iconValue);
+ usageValue.set_enum(usage);
+ set_property("usage", usageValue);
+ nicknameValue.set_string(simple);
+ set_property("nickname", nicknameValue);
+ identifierValue.set_string(identifier);
+ set_property("identifier", identifierValue);
+ flagsValue.set_uint((uint) flags);
+ set_property("object-flags", flagsValue);
+ }
+
+ public void refresh() {
+ // TODO: Need to work on key refreshing
+ }
+
+ public GLib.List<Seahorse.Exporter> create_exporters(ExporterType type) {
+ List<Seahorse.Exporter> exporters = new List<Seahorse.Exporter>();
+ exporters.append(new Exporter(this, false));
+ return exporters;
+ }
+
+ public Seahorse.Deleter create_deleter() {
+ return new Deleter(this);
+ }
+
+ public Gtk.Window? create_viewer(Gtk.Window? parent) {
+ KeyProperties properties_dialog = new KeyProperties(this, parent);
+ properties_dialog.show();
+ return properties_dialog;
+ }
+
+ public Algorithm get_algo() {
+ return this.key_data.algo;
+ }
+
+ public string? get_location() {
+ if (this.key_data == null)
+ return null;
+ return this.key_data.get_location();
+ }
+
+ public uint get_strength() {
+ return (this.key_data != null)? this.key_data.length : 0;
+ }
+
+ /**
+ * Creates a valid identifier for an SSH key from a given string.
+ *
+ * @return A valid identifier, or null if the result is too short.
+ */
+ public static string? calc_identifier(string id) {
+ // Strip out all non-alphanumeric chars and limit length to SSH_ID_SIZE
+ try {
+ Regex regex = new Regex("[^a-zA-Z0-9]");
+ string result = regex.replace(id, id.length, 0, "");
+
+ if (result.length >= SSH_IDENTIFIER_SIZE)
+ return result.substring(0, result.length);
+ } catch (RegexError e) {
+ warning("Couldn't create regex for calc_identifier. Message: %s".printf(e.message));
+ }
+
+ return null;
+ }
+
+ /**
+ * Sometimes keys loaded later on have more information (e.g. keys loaded
+ * from authorized_keys), so propagate that up to the previously loaded key.
+ */
+ public void merge_keydata(KeyData keydata) {
+ if (!this.key_data.authorized && keydata.authorized) {
+ this.key_data.authorized = true;
+
+ // Let the key know something's changed
+ this.key_data = this.key_data;
+ }
+ }
+
+ /**
+ * Parses a string into public/private keys.
+ *
+ * @param data The string that needs to be parsed.
+ * @param pub_handler Will be called anytime a public key has been parsed.
+ * If null, nothing will be done if a public key is parsed.
+ * @param priv_handler Will be called anytime a private key has been parsed.
+ * If null, nothing will be done if a private key is parsed.
+ * @param cancellable Can be used to cancel the parsing.
+ */
+ public static async void parse(string data,
+ PubParsedHandler? pub_handler,
+ PrivParsedHandler? priv_handler = null,
+ Cancellable? cancellable = null) throws GLib.Error {
+ if (data == null || data == "")
+ return;
+
+ StringBuilder toParse = new StringBuilder(data.chug());
+ while (toParse.str.length > 0) {
+ // First of all, check for a private key, as it can span several lines
+ if (SecData.contains_private_key(toParse.str)) {
+ try {
+ SecData secdata = SecData.parse_data(toParse);
+ if (priv_handler != null)
+ priv_handler(secdata);
+ continue;
+ } catch (GLib.Error e) {
+ warning(e.message);
+ }
+ }
+
+ // We're sure we'll have at least 1 element
+ string[] lines = toParse.str.split("\n", 2);
+ string line = lines[0];
+ toParse.erase(0, line.length);
+ if (lines.length == 2) // There was a \n, so don't forget to erase it as well
+ toParse.erase(0, 1);
+
+ // Comments and empty lines, not a parse error, but no data
+ if (line.strip() == "" || line.has_prefix("#"))
+ continue;
+
+ // See if we have a public key
+ try {
+ KeyData keydata = KeyData.parse_line(line);
+ if (pub_handler != null)
+ pub_handler(keydata);
+ } catch (GLib.Error e) {
+ warning(e.message);
+ }
+ }
+ }
+
+ /**
+ * Parses the contents of the given file into public/private keys.
+ *
+ * @param data The file that will be parsed.
+ * @param pub_handler Will be called anytime a public key has been parsed.
+ * If null, nothing will be done if a public key is parsed.
+ * @param priv_handler Will be called anytime a private key has been parsed.
+ * If null, nothing will be done if a private key is parsed.
+ * @param cancellable Can be used to cancel the parsing.
+ */
+ public static async void parse_file(string filename,
+ PubParsedHandler? pub_handler,
+ PrivParsedHandler? priv_handler = null,
+ Cancellable? cancellable = null) throws GLib.Error {
+ string contents;
+ FileUtils.get_contents(filename, out contents);
+
+ yield parse(contents, pub_handler, priv_handler, cancellable);
+ }
+
+ /**
+ * Takes care of the public key that has been found in a string while parsing.
+ */
+ public delegate bool PubParsedHandler(KeyData data);
+
+ /**
+ * Takes care of the private key that has been found in a string while parsing.
+ */
+ public delegate bool PrivParsedHandler(SecData data);
+}
diff --git a/ssh/operation.vala b/ssh/operation.vala
new file mode 100644
index 0000000..174e91d
--- /dev/null
+++ b/ssh/operation.vala
@@ -0,0 +1,397 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2006 Stefan Walter
+ * Copyright (C) 2011 Collabora Ltd.
+ * Copyright (C) 2016 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+namespace Seahorse.Ssh {
+
+/**
+ * Wraps a command-line executable in its own async method.
+ *
+ * The basic idea is that we don't want to link to the OpenSSL/LibreSSL libraries.
+ * So, create a subclass instead and do whatever you want with the output.
+ */
+public abstract class Operation : GLib.Object {
+
+ private string command_name;
+
+ private StringBuilder _std_out = new StringBuilder();
+ protected StringBuilder std_out { get { return _std_out; } }
+
+ private StringBuilder _std_err = new StringBuilder();
+ protected StringBuilder std_err { get { return _std_err; } }
+
+ private Pid pid;
+
+ // These are all fields that will be used in case the user will be prompted.
+ protected string? prompt_title;
+ protected string? prompt_message;
+ protected string? prompt_argument;
+ protected ulong prompt_transient_for;
+
+ /**
+ * Calls a command and returns the output.
+ *
+ * @param command The command that should be launched.
+ * @param input The standard input for the command, or null if none expected.
+ * @param cancellable Can be used if you want to cancel. The process will be killed.
+ * @return The output of the command.
+ */
+ protected async void operation_async(string command,
+ string? input,
+ Cancellable? cancellable) throws GLib.Error {
+ if (command == null || command == "")
+ return;
+
+ string[] args;
+ try {
+ Shell.parse_argv(command, out args);
+ this.command_name = args[0];
+ } catch (GLib.Error e) {
+ critical("Couldn't parse SSH command line %s.", command);
+ throw e;
+ }
+
+ debug("SSHOP: Executing SSH command: %s", command);
+
+ // And off we go to run the program
+ int? fin, fout, ferr;
+ if (input != null)
+ fin = null;
+ Process.spawn_async_with_pipes(null, args, null,
+ SpawnFlags.DO_NOT_REAP_CHILD | SpawnFlags.LEAVE_DESCRIPTORS_OPEN,
+ on_spawn_setup_child, out this.pid, out fin, out fout, out ferr);
+
+ ulong cancelled_sig = 0;
+ if (cancellable != null)
+ cancelled_sig = cancellable.connect(() => { Posix.kill(this.pid, Posix.SIGTERM); });
+
+ // Copy the input for later writing
+ if (input != null) {
+ StringBuilder std_in = new StringBuilder(input);
+ debug("Sending input to '%s': '%s'", this.command_name, std_in.str);
+ create_io_channel(std_in, fin, IOCondition.OUT,
+ (io, cond) => on_io_ssh_write(io, cond, std_in));
+ }
+
+ // Make all the proper IO Channels for the output/error
+ create_io_channel(this.std_out, fout, IOCondition.IN,
+ (io, cond) => on_io_ssh_read(io, cond, this.std_out));
+ create_io_channel(this.std_err, ferr, IOCondition.IN,
+ (io, cond) => on_io_ssh_read(io, cond, this.std_err));
+
+ // Process watch
+ watch_ssh_process(cancellable, cancelled_sig);
+ }
+
+ private void create_io_channel(StringBuilder data,
+ int handle,
+ IOCondition io_cond,
+ IOFunc io_func) throws GLib.Error {
+ Posix.fcntl(handle, Posix.F_SETFL, Posix.O_NONBLOCK | Posix.fcntl(handle, Posix.F_GETFL));
+ IOChannel io_channel = new IOChannel.unix_new(handle);
+ io_channel.set_encoding(null);
+ io_channel.set_close_on_unref(true);
+ io_channel.add_watch(io_cond, io_func);
+ }
+
+ private void watch_ssh_process(Cancellable? cancellable, ulong cancelled_sig) throws GLib.Error {
+ ChildWatch.add_full(Priority.DEFAULT, this.pid, (pid, status) => {
+ debug("Process '%s' done.", this.command_name);
+
+ // FIXME This is the wrong place to catch that error really
+ try {
+ Process.check_exit_status(status);
+ } catch (GLib.Error e) {
+ Seahorse.Util.show_error(null, this.prompt_title, this.std_err.str);
+ }
+ cancellable.disconnect(cancelled_sig);
+ Process.close_pid(pid);
+ });
+ }
+
+ /**
+ * Escapes a command-line argument so it can be safely passed on.
+ *
+ * @param arg The argument that should be escaped.
+ * @return The escaped argument.
+ */
+ protected string escape_shell_arg (string arg) {
+ string escaped = arg.replace("'", "\'");
+ return "\'%s\'".printf(escaped);
+ }
+
+ private bool on_io_ssh_read(IOChannel source, IOCondition condition, StringBuilder data) {
+ try {
+ IOStatus status = IOStatus.NORMAL;
+ while (status != IOStatus.EOF) {
+ string buf;
+ status = source.read_line(out buf, null, null);
+
+ if (status == IOStatus.NORMAL && buf != null) {
+ data.append(buf);
+ debug("%s", data.str.substring(buf.length));
+ }
+ }
+ } catch (GLib.Error e) {
+ critical("Couldn't read output of SSH command. Error: %s", e.message);
+ Posix.kill(this.pid, Posix.SIGTERM);
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool on_io_ssh_write(IOChannel source, IOCondition condition, StringBuilder input) {
+ debug("SSHOP: SSH ready for input");
+ try {
+ size_t written = 0;
+ IOStatus status = source.write_chars(input.str.to_utf8(), out written);
+ if (status != IOStatus.AGAIN) {
+ debug("SSHOP: Wrote %u bytes to SSH", (uint) written);
+ input.erase(0, (ssize_t) written);
+ }
+ } catch (GLib.Error e) {
+ critical("Couldn't write to STDIN of SSH command. Error: %s", e.message);
+ Posix.kill(this.pid, Posix.SIGTERM);
+ return false;
+ }
+
+ return true;
+ }
+
+ private void on_spawn_setup_child() {
+ // No terminal for this process
+ Posix.setsid();
+
+ Environment.set_variable("SSH_ASKPASS", "%sseahorse-ssh-askpass".printf(Config.EXECDIR), false);
+
+ // We do screen scraping so we need locale C
+ if (Environment.get_variable("LC_ALL") != null)
+ Environment.set_variable("LC_ALL", "C", true);
+ Environment.set_variable("LANG", "C", true);
+
+ if (this.prompt_title != null)
+ Environment.set_variable("SEAHORSE_SSH_ASKPASS_TITLE", prompt_title, true);
+ if (this.prompt_message != null)
+ Environment.set_variable("SEAHORSE_SSH_ASKPASS_MESSAGE", prompt_message, true);
+ if (this.prompt_transient_for != 0) {
+ string parent = "%lu".printf(prompt_transient_for);
+ Environment.set_variable("SEAHORSE_SSH_ASKPASS_PARENT", parent, true);
+ }
+ }
+}
+
+public class UploadOperation : Operation {
+ /**
+ * Uploads a set of keys to a given host.
+ *
+ * @param keys The keys that should be uploaded.
+ * @param username The username to use on the server.
+ * @param hostname The URL of the host.
+ * @param port The port of the host. If none is specified, the default port is used.
+ * @param cancellable Used if you want to cancel the operation.
+ */
+ public async void upload_async(List<Key> keys,
+ string username, string hostname, string? port,
+ Cancellable? cancellable) throws GLib.Error {
+ this.prompt_title = _("Remote Host Password");
+
+ if (keys == null
+ || username == null || username == ""
+ || hostname == null || hostname == "")
+ return;
+
+ if (port == null)
+ port = "";
+
+ StringBuilder data = new StringBuilder.sized(1024);
+ keys.foreach((key) => {
+ KeyData keydata = key.key_data;
+ if (keydata != null && keydata.pubfile != null) {
+ data.append(keydata.rawdata);
+ data.append_c('\n');
+ }
+ });
+
+ /*
+ * This command creates the .ssh directory if necessary (with appropriate permissions)
+ * and then appends all input data onto the end of .ssh/authorized_keys
+ */
+ // TODO: Important, we should handle the host checking properly
+ string cmd = "%s '%s@%s' %s %s -o StrictHostKeyChecking=no \"umask 077; test -d .ssh || mkdir .ssh ;
cat >> .ssh/authorized_keys\""
+ .printf(Config.SSH_PATH, username, hostname, port != "" ? "-p" : "", port);
+
+ yield operation_async(cmd, data.str, cancellable);
+ }
+}
+
+public class ChangePassphraseOperation : Operation {
+ /**
+ * Changes the passphrase of the given key.
+ *
+ * @param key The key of which to change the passphrase.
+ * @param cancellable Used if you want to cancel the operation.
+ */
+ public async void change_passphrase_async(Key key,
+ Cancellable? cancellable) throws GLib.Error {
+ if (key.key_data == null || key.key_data.privfile == null)
+ return;
+
+ this.prompt_title = _("Enter Key Passphrase");
+ this.prompt_argument = key.label;
+
+ string cmd = "%s -p -f '%s'".printf(Config.SSH_KEYGEN_PATH, key.key_data.privfile);
+ yield operation_async(cmd, null, cancellable);
+ }
+}
+
+public class GenerateOperation : Operation {
+ /**
+ * Generates an SSH key pair.
+ *
+ * @param filename The filename of the new key.
+ * @param email The e-mail address for which the key should be made.
+ * @param type The type of key, i.e. which algorithm should be used;
+ * @param bits The amount of bits that should be used.
+ * @param cancellable Used if you want to cancel the operation.
+ */
+ public async void generate_async(string filename,
+ string email,
+ Algorithm type,
+ uint bits,
+ Cancellable cancellable) throws GLib.Error {
+ if (type == Algorithm.UNKNOWN)
+ throw new Error.GENERAL("Can't generate key for an unknown algorithm");
+
+ this.prompt_title = _("Passphrase for New Secure Shell Key");
+
+ string comment = escape_shell_arg(email);
+ string algo = type.to_string().down();
+
+ // Default number of bits
+ if (bits == 0)
+ bits = 2048;
+
+ string cmd = "%s -b '%u' -t '%s' -C %s -f '%s'".printf(Config.SSH_KEYGEN_PATH, bits, algo, comment,
filename);
+
+ yield operation_async(cmd, null, cancellable);
+ }
+}
+
+public class PrivateImportOperation : Operation {
+ /**
+ * Imports a private key into a file.
+ *
+ * @param source The source that will decide the location of the private key.
+ * @param data The public key that needs to be imported.
+ * @param filename The name of the file in which the key should be imported.
+ * @param cancellable Allows the operation to be cancelled.
+ */
+ public async string? import_private_async(Source source,
+ SecData data,
+ string? filename,
+ Cancellable cancellable) throws GLib.Error {
+ if (data == null || data.rawdata == null)
+ throw new Error.GENERAL("Trying to import private key that is empty.");
+
+ // No filename specified, make one up
+ string file = filename ?? source.new_filename_for_algorithm(data.algo);
+
+ // Add the comment to the output
+ string message = (data.comment != null) ?
+ _("Importing key: %s").printf(data.comment) : _("Importing key. Enter passphrase");
+
+ // The prompt
+ this.prompt_title = _("Import Key");
+ this.prompt_message = message;
+
+ // Write the private key into the file
+ FileUtils.set_contents(file, data.rawdata);
+
+ // Start command to generate public key
+ string cmd = "%s -y -f '%s'".printf(Config.SSH_KEYGEN_PATH, file);
+ yield operation_async(cmd, null, cancellable);
+
+ // Only use the first line of the output
+ int pos = int.max(this.std_out.str.index_of_char('\n'), std_out.str.index_of_char('\r'));
+ if (pos != -1)
+ std_out.erase(pos);
+
+ // Parse the data so we can get the fingerprint
+ KeyData? keydata = KeyData.parse_line(std_out.str);
+
+ // Add the comment to the output
+ if (data.comment != null) {
+ std_out.append_c(' ');
+ std_out.append(data.comment);
+ }
+
+ // The file to write to
+ string pubfile = "%s.pub".printf(file);
+ FileUtils.set_contents(pubfile, std_out.str);
+
+ if (keydata != null && keydata.is_valid())
+ return keydata.fingerprint;
+
+ throw new Error.GENERAL("Couldn't parse imported private key fingerprint");
+ }
+}
+
+public class RenameOperation : Operation {
+ /**
+ * Renames/Deletes the comment in a key.
+ *
+ * @param key The key that should have its comment renamed/deleted.
+ * @param new_comment The new comment of the key (or null if the previous
+ * comment should be deleted)
+ */
+ public async void rename_async(Key key, string? new_comment, Gtk.Window transient_for) throws GLib.Error
{
+ KeyData keydata = key.key_data;
+
+ if (new_comment == null)
+ new_comment = "";
+
+ if (!change_raw_comment (keydata, new_comment))
+ return;
+
+ debug("Renaming key to: %s", new_comment);
+
+ assert (keydata.pubfile != null);
+ if (keydata.partial) // Just part of a file for this key
+ KeyData.filter_file (keydata.pubfile, keydata, keydata);
+ else // A full file for this key
+ FileUtils.set_contents(keydata.pubfile, keydata.rawdata);
+ }
+
+ private bool change_raw_comment(KeyData keydata, string new_comment) {
+ assert(keydata.rawdata != null);
+
+ string no_leading = keydata.rawdata.chug();
+ string[] parts = no_leading.split_set(" ", 3);
+ if (parts.length < 3)
+ return false;
+
+ keydata.rawdata = parts[0] + " " + parts[1] + " " + new_comment;
+
+ return true;
+ }
+}
+
+}
diff --git a/ssh/seahorse-ssh-askpass.c b/ssh/seahorse-ssh-askpass.c
index 4617d73..cccf59f 100644
--- a/ssh/seahorse-ssh-askpass.c
+++ b/ssh/seahorse-ssh-askpass.c
@@ -141,3 +141,4 @@ main (int argc, char* argv[])
gtk_widget_destroy (GTK_WIDGET (dialog));
return result;
}
+
diff --git a/ssh/seahorse-ssh-generate.ui b/ssh/seahorse-ssh-generate.ui
index d918617..fc34a0b 100644
--- a/ssh/seahorse-ssh-generate.ui
+++ b/ssh/seahorse-ssh-generate.ui
@@ -8,177 +8,64 @@
<property name="step_increment">256</property>
<property name="page_increment">10</property>
</object>
- <object class="GtkImage" id="create-image">
- <property name="can_focus">False</property>
- <property name="stock">gtk-ok</property>
- </object>
- <object class="GtkDialog" id="ssh-generate">
+ <object class="GtkBox" id="ssh-generate">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="border_width">5</property>
- <property name="title" translatable="yes">New Secure Shell Key</property>
- <property name="resizable">False</property>
- <property name="modal">True</property>
- <property name="window_position">center-on-parent</property>
- <property name="type_hint">dialog</property>
- <signal name="delete-event" handler="on_widget_delete_event" swapped="no"/>
- <child internal-child="vbox">
- <object class="GtkBox" id="dialog-vbox1">
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkBox" id="hbox4">
<property name="visible">True</property>
+ <property name="orientation">horizontal</property>
<property name="can_focus">False</property>
- <property name="orientation">vertical</property>
- <property name="spacing">2</property>
- <child internal-child="action_area">
- <object class="GtkButtonBox" id="dialog-action_area1">
+ <property name="border_width">5</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkAlignment" id="alignment9">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="layout_style">end</property>
- <child>
- <object class="GtkButton" id="helpbutton1">
- <property name="label" translatable="yes">_Just Create Key</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="can_default">True</property>
- <property name="receives_default">False</property>
- <property name="use_action_appearance">False</property>
- <property name="use_underline">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
<child>
- <object class="GtkButton" id="button1">
- <property name="label">gtk-cancel</property>
+ <object class="GtkImage" id="ssh-image">
<property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="can_default">True</property>
- <property name="receives_default">False</property>
- <property name="use_action_appearance">False</property>
- <property name="use_stock">True</property>
+ <property name="can_focus">False</property>
+ <property name="yalign">0</property>
+ <property name="pixel_size">48</property>
+ <property name="icon_name">gcr-key-pair</property>
</object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="button2">
- <property name="label" translatable="yes">_Create and Set Up</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="can_default">True</property>
- <property name="has_default">True</property>
- <property name="receives_default">False</property>
- <property name="use_action_appearance">False</property>
- <property name="image">create-image</property>
- <property name="use_underline">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">2</property>
- </packing>
</child>
</object>
<packing>
- <property name="expand">False</property>
+ <property name="expand">True</property>
<property name="fill">True</property>
- <property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
- <object class="GtkHBox" id="hbox4">
+ <object class="GtkBox" id="vbox5">
<property name="visible">True</property>
+ <property name="orientation">vertical</property>
<property name="can_focus">False</property>
- <property name="border_width">5</property>
<property name="spacing">12</property>
<child>
- <object class="GtkAlignment" id="alignment9">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkImage" id="ssh-image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="yalign">0</property>
- <property name="pixel_size">48</property>
- <property name="icon_name">gcr-key-pair</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">True</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkVBox" id="vbox5">
+ <object class="GtkBox" id="vbox3">
<property name="visible">True</property>
+ <property name="orientation">vertical</property>
<property name="can_focus">False</property>
<property name="spacing">12</property>
<child>
- <object class="GtkVBox" id="vbox3">
+ <object class="GtkBox" id="hbox41">
<property name="visible">True</property>
+ <property name="orientation">horizontal</property>
<property name="can_focus">False</property>
<property name="spacing">12</property>
<child>
- <object class="GtkHBox" id="hbox41">
+ <object class="GtkLabel" id="label45">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="spacing">12</property>
- <child>
- <object class="GtkLabel" id="label45">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="xalign">0</property>
- <property name="yalign">0</property>
- <property name="label" translatable="yes">A Secure Shell (SSH) key lets you
connect securely to other computers.</property>
- <property name="wrap">True</property>
- </object>
- <packing>
- <property name="expand">True</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkAlignment" id="alignment8">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="xalign">0</property>
- <property name="yalign">0</property>
- <property name="xscale">0</property>
- <property name="yscale">0</property>
- <child>
- <object class="GtkButton" id="button3">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="use_action_appearance">False</property>
- <property name="relief">half</property>
- <signal name="clicked" handler="on_widget_help" swapped="no"/>
- <child>
- <object class="GtkImage" id="image3">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="stock">gtk-help</property>
- </object>
- </child>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">1</property>
- </packing>
- </child>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes">A Secure Shell (SSH) key lets you connect
securely to other computers.</property>
+ <property name="wrap">True</property>
</object>
<packing>
<property name="expand">True</property>
@@ -187,77 +74,34 @@
</packing>
</child>
<child>
- <object class="GtkAlignment" id="alignment7">
+ <object class="GtkAlignment" id="alignment8">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
<child>
- <object class="GtkGrid" id="grid1">
+ <object class="GtkButton" id="button3">
<property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="row_spacing">6</property>
- <property name="column_spacing">12</property>
- <child>
- <object class="GtkLabel" id="label46">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="xalign">1</property>
- <property name="yalign">0</property>
- <property name="label" translatable="yes">_Description:</property>
- <property name="use_markup">True</property>
- <property name="use_underline">True</property>
- <property name="mnemonic_widget">email-entry</property>
- </object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">0</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="email-entry">
- <property name="width_request">180</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="invisible_char">●</property>
- <property name="activates_default">True</property>
- <property name="invisible_char_set">True</property>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">0</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
- </child>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_action_appearance">False</property>
+ <property name="relief">half</property>
+ <signal name="clicked" handler="on_widget_help" swapped="no"/>
<child>
- <object class="GtkLabel" id="label53">
+ <object class="GtkImage" id="image3">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="xalign">0</property>
- <property name="xpad">3</property>
- <property name="label" translatable="yes">Your email address, or a reminder
of what this key is for.</property>
- <property name="wrap">True</property>
- <attributes>
- <attribute name="style" value="italic"/>
- </attributes>
+ <property name="stock">gtk-help</property>
</object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">1</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
- </child>
- <child>
- <placeholder/>
</child>
</object>
</child>
</object>
<packing>
- <property name="expand">True</property>
- <property name="fill">True</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
@@ -269,24 +113,25 @@
</packing>
</child>
<child>
- <object class="GtkExpander" id="expander1">
+ <object class="GtkAlignment" id="alignment7">
<property name="visible">True</property>
- <property name="can_focus">True</property>
+ <property name="can_focus">False</property>
<child>
- <object class="GtkGrid" id="grid2">
+ <object class="GtkGrid" id="grid1">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="margin_left">20</property>
<property name="row_spacing">6</property>
<property name="column_spacing">12</property>
<child>
- <object class="GtkLabel" id="label49">
+ <object class="GtkLabel" id="label46">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">Encryption _Type:</property>
+ <property name="xalign">1</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes">_Description:</property>
+ <property name="use_markup">True</property>
<property name="use_underline">True</property>
- <property name="mnemonic_widget">algorithm-choice</property>
+ <property name="mnemonic_widget">email-entry</property>
</object>
<packing>
<property name="left_attach">0</property>
@@ -296,40 +141,13 @@
</packing>
</child>
<child>
- <object class="GtkLabel" id="label50">
+ <object class="GtkEntry" id="email-entry">
+ <property name="width_request">180</property>
<property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">Key _Strength (bits):</property>
- <property name="use_underline">True</property>
- <property name="mnemonic_widget">bits-entry</property>
- </object>
- <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">1</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkAlignment" id="alignment4">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="xalign">0</property>
- <property name="yalign">0</property>
- <property name="xscale">0</property>
- <child>
- <object class="GtkComboBoxText" id="algorithm-choice">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="entry_text_column">0</property>
- <property name="id_column">1</property>
- <items>
- <item translatable="yes">RSA</item>
- <item translatable="yes">DSA</item>
- </items>
- </object>
- </child>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="activates_default">True</property>
+ <property name="invisible_char_set">True</property>
</object>
<packing>
<property name="left_attach">1</property>
@@ -339,24 +157,16 @@
</packing>
</child>
<child>
- <object class="GtkAlignment" id="alignment5">
+ <object class="GtkLabel" id="label53">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
- <property name="yalign">0</property>
- <property name="xscale">0</property>
- <property name="yscale">0</property>
- <child>
- <object class="GtkSpinButton" id="bits-entry">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="invisible_char">●</property>
- <property name="invisible_char_set">True</property>
- <property name="adjustment">adjustment1</property>
- <property name="climb_rate">1</property>
- <property name="numeric">True</property>
- </object>
- </child>
+ <property name="xpad">3</property>
+ <property name="label" translatable="yes">Your email address, or a reminder of
what this key is for.</property>
+ <property name="wrap">True</property>
+ <attributes>
+ <attribute name="style" value="italic"/>
+ </attributes>
</object>
<packing>
<property name="left_attach">1</property>
@@ -365,17 +175,9 @@
<property name="height">1</property>
</packing>
</child>
- </object>
- </child>
- <child type="label">
- <object class="GtkLabel" id="label48">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label" translatable="yes">_Advanced key options</property>
- <property name="use_underline">True</property>
- <attributes>
- <attribute name="weight" value="bold"/>
- </attributes>
+ <child>
+ <placeholder/>
+ </child>
</object>
</child>
</object>
@@ -385,19 +187,122 @@
<property name="position">1</property>
</packing>
</child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkExpander" id="expander1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
<child>
- <object class="GtkLabel" id="label54">
+ <object class="GtkGrid" id="grid2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">20</property>
+ <property name="row_spacing">6</property>
+ <property name="column_spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="label49">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Encryption _Type:</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">algorithm-combo-box</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label50">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Key _Strength (bits):</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">bits-spin-button</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="alignment4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="xscale">0</property>
+ <child>
+ <object class="GtkComboBoxText" id="algorithm-combo-box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="entry_text_column">0</property>
+ <property name="id_column">1</property>
+ <items>
+ <item translatable="yes">RSA</item>
+ <item translatable="yes">DSA</item>
+ </items>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="alignment5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
+ <child>
+ <object class="GtkSpinButton" id="bits-spin-button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="invisible_char_set">True</property>
+ <property name="adjustment">adjustment1</property>
+ <property name="climb_rate">1</property>
+ <property name="numeric">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label48">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">If there is a computer you want to use this
key with, you can set up that computer to recognize your new key.</property>
- <property name="wrap">True</property>
+ <property name="label" translatable="yes">_Advanced key options</property>
+ <property name="use_underline">True</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
</object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">2</property>
- </packing>
</child>
</object>
<packing>
@@ -406,19 +311,63 @@
<property name="position">1</property>
</packing>
</child>
+ <child>
+ <object class="GtkLabel" id="label54">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">If there is a computer you want to use this key
with, you can set up that computer to recognize your new key.</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
</object>
<packing>
- <property name="expand">False</property>
+ <property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <object class="GtkButtonBox" id="action_area">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkButton" id="create-no-setup">
+ <property name="label" translatable="yes">_Just Create Key</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="create-with-setup">
+ <property name="label" translatable="yes">_Create and Set Up</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
</child>
- <action-widgets>
- <action-widget response="-7">helpbutton1</action-widget>
- <action-widget response="-6">button1</action-widget>
- <action-widget response="-5">button2</action-widget>
- </action-widgets>
</object>
</interface>
diff --git a/ssh/seahorse-ssh-key-properties.ui b/ssh/seahorse-ssh-key-properties.ui
index 24ef34d..933f181 100644
--- a/ssh/seahorse-ssh-key-properties.ui
+++ b/ssh/seahorse-ssh-key-properties.ui
@@ -8,568 +8,508 @@
<object class="GtkImage" id="export-image">
<property name="stock">gtk-save-as</property>
</object>
- <object class="GtkDialog" id="ssh-key-properties">
+ <object class="GtkBox" id="ssh-key-properties">
<property name="visible">True</property>
- <property name="border_width">5</property>
- <property name="title" translatable="yes">Key Properties</property>
- <property name="resizable">False</property>
- <property name="type_hint">dialog</property>
- <signal name="delete_event" handler="on_widget_delete_event"/>
- <child internal-child="vbox">
- <object class="GtkVBox" id="dialog-vbox1">
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkNotebook" id="notebook">
<property name="visible">True</property>
- <property name="orientation">vertical</property>
- <property name="spacing">2</property>
+ <property name="can_focus">True</property>
+ <property name="border_width">5</property>
<child>
- <object class="GtkNotebook" id="notebook">
+ <object class="GtkBox" id="vbox7">
<property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="border_width">5</property>
+ <property name="orientation">vertical</property>
+ <property name="border_width">12</property>
+ <property name="spacing">12</property>
<child>
- <object class="GtkVBox" id="vbox7">
+ <object class="GtkHBox" id="hbox61">
<property name="visible">True</property>
- <property name="border_width">12</property>
- <property name="orientation">vertical</property>
- <property name="spacing">12</property>
<child>
- <object class="GtkHBox" id="hbox61">
+ <object class="GtkImage" id="key-image">
<property name="visible">True</property>
+ <property name="yalign">0</property>
+ <property name="stock">gtk-missing-image</property>
+ <property name="icon-size">6</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTable" id="table9">
+ <property name="visible">True</property>
+ <property name="n_rows">4</property>
+ <property name="n_columns">2</property>
+ <property name="column_spacing">12</property>
+ <property name="row_spacing">3</property>
+ <child>
+ <object class="GtkLabel" id="label74">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">Identifier:</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="id-label">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="xalign">0</property>
+ <property name="label"></property>
+ <property name="selectable">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label93">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes">Type:</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="type-label">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Secure Shell Key</property>
+ <property name="selectable">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
<child>
- <object class="GtkImage" id="key-image">
+ <object class="GtkLabel" id="label72">
<property name="visible">True</property>
+ <property name="xalign">1</property>
<property name="yalign">0</property>
- <property name="stock">gtk-missing-image</property>
- <property name="icon-size">6</property>
+ <property name="label" translatable="yes" context="name-of-ssh-key" comments="Name
of key, often a persons name">Name:</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
</object>
<packing>
- <property name="expand">False</property>
- <property name="position">0</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
</packing>
</child>
<child>
- <object class="GtkTable" id="table9">
+ <object class="GtkEntry" id="comment-entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="activates_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label22227">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="yalign">1</property>
+ <property name="label" translatable="yes">Used to connect to other
computers.</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label22228">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="yalign">1</property>
+ <property name="label" translatable="yes" comments="To translators: This is the noun
not the verb.">Use:</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="alignment42">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
+ <child>
+ <object class="GtkButton" id="passphrase-button">
+ <property name="label" translatable="yes">Change _Passphrase</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="image">passphrase-image</property>
+ <property name="use_underline">True</property>
+ <signal name="clicked" handler="on_ssh_passphrase_button_clicked"/>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="vbox28">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label22226">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Trust</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="alignment46">
+ <property name="visible">True</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkBox" id="vbox29">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkCheckButton" id="trust-check">
+ <property name="label" translatable="yes">The owner of this key is _authorized
to connect to this computer</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="on_ssh_trust_toggled"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="trust-message">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">This only applies to the
<i>%s</i> account.</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="xpad">3</property>
+ <property name="label" translatable="yes">Key</property>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="vbox8">
+ <property name="visible">True</property>
+ <property name="border_width">12</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkFrame" id="frame4">
+ <property name="visible">True</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment29">
+ <property name="visible">True</property>
+ <property name="top_padding">6</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkTable" id="table10">
<property name="visible">True</property>
<property name="n_rows">4</property>
<property name="n_columns">2</property>
<property name="column_spacing">12</property>
- <property name="row_spacing">3</property>
+ <property name="row_spacing">6</property>
<child>
- <object class="GtkLabel" id="label74">
+ <object class="GtkLabel" id="label109">
<property name="visible">True</property>
- <property name="xalign">1</property>
- <property name="label" translatable="yes">Identifier:</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes">Algorithm:</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
</packing>
</child>
<child>
- <object class="GtkLabel" id="id-label">
+ <object class="GtkLabel" id="label25">
<property name="visible">True</property>
- <property name="can_focus">True</property>
<property name="xalign">0</property>
- <property name="label"></property>
- <property name="selectable">True</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes">Strength:</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
</object>
<packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
</packing>
</child>
<child>
- <object class="GtkLabel" id="label93">
+ <object class="GtkLabel" id="algo-label">
<property name="visible">True</property>
- <property name="xalign">1</property>
- <property name="yalign">0</property>
- <property name="label" translatable="yes">Type:</property>
- <attributes>
- <attribute name="weight" value="bold"/>
- </attributes>
+ <property name="can_focus">True</property>
+ <property name="xalign">0</property>
+ <property name="label"></property>
+ <property name="selectable">True</property>
</object>
<packing>
- <property name="top_attach">2</property>
- <property name="bottom_attach">3</property>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
</packing>
</child>
<child>
- <object class="GtkLabel" id="type-label">
+ <object class="GtkLabel" id="strength-label">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="xalign">0</property>
- <property name="label" translatable="yes">Secure Shell Key</property>
+ <property name="label"></property>
<property name="selectable">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
- <property name="top_attach">2</property>
- <property name="bottom_attach">3</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
</packing>
</child>
<child>
- <object class="GtkLabel" id="label72">
+ <object class="GtkLabel" id="label113">
<property name="visible">True</property>
- <property name="xalign">1</property>
- <property name="yalign">0</property>
- <property name="label" translatable="yes" context="name-of-ssh-key"
comments="Name of key, often a persons name">Name:</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Location:</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
</packing>
</child>
<child>
- <object class="GtkEntry" id="comment-entry">
+ <object class="GtkLabel" id="label22231">
<property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="activates_default">True</property>
- <signal name="focus_out_event" handler="on_ssh_comment_focus_out"/>
- <signal name="activate" handler="on_ssh_comment_activate"/>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Fingerprint:</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
</object>
<packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
</packing>
</child>
<child>
- <object class="GtkLabel" id="label22227">
+ <object class="GtkLabel" id="fingerprint-label">
<property name="visible">True</property>
+ <property name="can_focus">True</property>
<property name="xalign">0</property>
- <property name="yalign">1</property>
- <property name="label" translatable="yes">Used to connect to other
computers.</property>
- <property name="use_markup">True</property>
+ <property name="yalign">0</property>
+ <property name="label"></property>
+ <property name="selectable">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
<property name="x_options">GTK_FILL</property>
- <property name="y_options">GTK_FILL</property>
+ <property name="y_options"></property>
</packing>
</child>
<child>
- <object class="GtkLabel" id="label22228">
+ <object class="GtkLabel" id="location-label">
<property name="visible">True</property>
- <property name="xalign">1</property>
- <property name="yalign">1</property>
- <property name="label" translatable="yes" comments="To translators: This is the
noun not the verb.">Use:</property>
- <attributes>
- <attribute name="weight" value="bold"/>
- </attributes>
+ <property name="can_focus">True</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="label"></property>
+ <property name="use_markup">True</property>
+ <property name="selectable">True</property>
+ <property name="ellipsize">start</property>
</object>
<packing>
- <property name="x_options">GTK_FILL</property>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
<property name="y_options"></property>
</packing>
</child>
</object>
- <packing>
- <property name="position">1</property>
- </packing>
</child>
</object>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkAlignment" id="alignment42">
- <property name="visible">True</property>
- <property name="xalign">1</property>
- <property name="xscale">0</property>
- <property name="yscale">0</property>
- <child>
- <object class="GtkButton" id="passphrase-button">
- <property name="label" translatable="yes">Change _Passphrase</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="image">passphrase-image</property>
- <property name="use_underline">True</property>
- <signal name="clicked" handler="on_ssh_passphrase_button_clicked"/>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">1</property>
- </packing>
</child>
- <child>
- <object class="GtkVBox" id="vbox28">
+ <child type="label">
+ <object class="GtkLabel" id="label108">
<property name="visible">True</property>
- <property name="orientation">vertical</property>
- <property name="spacing">6</property>
- <child>
- <object class="GtkLabel" id="label22226">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">Trust</property>
- <attributes>
- <attribute name="weight" value="bold"/>
- </attributes>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkAlignment" id="alignment46">
- <property name="visible">True</property>
- <property name="left_padding">12</property>
- <child>
- <object class="GtkVBox" id="vbox29">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <property name="spacing">6</property>
- <child>
- <object class="GtkCheckButton" id="trust-check">
- <property name="label" translatable="yes">The owner of this key is
_authorized to connect to this computer</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="use_underline">True</property>
- <property name="draw_indicator">True</property>
- <signal name="toggled" handler="on_ssh_trust_toggled"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="trust-message">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">This only applies to the
<i>%s</i> account.</property>
- <property name="use_markup">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
+ <property name="label" translatable="yes">Technical Details:</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
</object>
- <packing>
- <property name="position">2</property>
- </packing>
</child>
</object>
<packing>
- <property name="tab_fill">False</property>
- </packing>
- </child>
- <child type="tab">
- <object class="GtkLabel" id="label1">
- <property name="visible">True</property>
- <property name="xpad">3</property>
- <property name="label" translatable="yes">Key</property>
- </object>
- <packing>
- <property name="tab_fill">False</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
</packing>
</child>
<child>
- <object class="GtkVBox" id="vbox8">
+ <object class="GtkAlignment" id="alignment44">
<property name="visible">True</property>
- <property name="border_width">12</property>
- <property name="orientation">vertical</property>
- <property name="spacing">12</property>
+ <property name="xalign">1</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
<child>
- <object class="GtkFrame" id="frame4">
+ <object class="GtkButton" id="export-button">
+ <property name="label" translatable="yes">E_xport Complete Key</property>
<property name="visible">True</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">none</property>
- <child>
- <object class="GtkAlignment" id="alignment29">
- <property name="visible">True</property>
- <property name="top_padding">6</property>
- <property name="left_padding">12</property>
- <child>
- <object class="GtkTable" id="table10">
- <property name="visible">True</property>
- <property name="n_rows">4</property>
- <property name="n_columns">2</property>
- <property name="column_spacing">12</property>
- <property name="row_spacing">6</property>
- <child>
- <object class="GtkLabel" id="label109">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="yalign">0</property>
- <property name="label" translatable="yes">Algorithm:</property>
- <attributes>
- <attribute name="weight" value="bold"/>
- </attributes>
- </object>
- <packing>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="label25">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="yalign">0</property>
- <property name="label" translatable="yes">Strength:</property>
- <attributes>
- <attribute name="weight" value="bold"/>
- </attributes>
- </object>
- <packing>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="algo-label">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="xalign">0</property>
- <property name="label"></property>
- <property name="selectable">True</property>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="strength-label">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="xalign">0</property>
- <property name="label"></property>
- <property name="selectable">True</property>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="label113">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">Location:</property>
- <attributes>
- <attribute name="weight" value="bold"/>
- </attributes>
- </object>
- <packing>
- <property name="top_attach">2</property>
- <property name="bottom_attach">3</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="label22231">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">Fingerprint:</property>
- <attributes>
- <attribute name="weight" value="bold"/>
- </attributes>
- </object>
- <packing>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="fingerprint-label">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="xalign">0</property>
- <property name="yalign">0</property>
- <property name="label"></property>
- <property name="selectable">True</property>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">3</property>
- <property name="bottom_attach">4</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="location-label">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="xalign">0</property>
- <property name="yalign">0</property>
- <property name="label"></property>
- <property name="use_markup">True</property>
- <property name="selectable">True</property>
- <property name="ellipsize">start</property>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">2</property>
- <property name="bottom_attach">3</property>
- <property name="y_options"></property>
- </packing>
- </child>
- </object>
- </child>
- </object>
- </child>
- <child type="label">
- <object class="GtkLabel" id="label108">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Technical Details:</property>
- <attributes>
- <attribute name="weight" value="bold"/>
- </attributes>
- </object>
- </child>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="image">export-image</property>
+ <property name="use_underline">True</property>
</object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkAlignment" id="alignment44">
- <property name="visible">True</property>
- <property name="xalign">1</property>
- <property name="xscale">0</property>
- <property name="yscale">0</property>
- <child>
- <object class="GtkButton" id="export-button">
- <property name="label" translatable="yes">E_xport Complete Key</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="image">export-image</property>
- <property name="use_underline">True</property>
- <signal name="clicked" handler="on_ssh_export_button_clicked"/>
- </object>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
- <child type="tab">
- <object class="GtkLabel" id="label22222">
- <property name="visible">True</property>
- <property name="xpad">3</property>
- <property name="label" translatable="yes">Details</property>
- </object>
- <packing>
- <property name="position">1</property>
- <property name="tab_fill">False</property>
- </packing>
- </child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
- <child internal-child="action_area">
- <object class="GtkHButtonBox" id="dialog-action_area1">
+ <child type="tab">
+ <object class="GtkLabel" id="label22222">
<property name="visible">True</property>
- <property name="layout_style">end</property>
- <child>
- <object class="GtkButton" id="helpbutton1">
- <property name="label">gtk-help</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="can_default">True</property>
- <property name="receives_default">False</property>
- <property name="use_stock">True</property>
- <signal name="clicked" handler="on_widget_help"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="closebutton1">
- <property name="label">gtk-close</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="can_default">True</property>
- <property name="receives_default">False</property>
- <property name="use_stock">True</property>
- <signal name="clicked" handler="on_widget_closed"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">1</property>
- </packing>
- </child>
+ <property name="xpad">3</property>
+ <property name="label" translatable="yes">Details</property>
</object>
<packing>
- <property name="expand">False</property>
- <property name="pack_type">end</property>
- <property name="position">0</property>
+ <property name="position">1</property>
+ <property name="tab_fill">False</property>
</packing>
</child>
</object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
</child>
- <action-widgets>
- <action-widget response="-11">helpbutton1</action-widget>
- <action-widget response="-7">closebutton1</action-widget>
- </action-widgets>
</object>
</interface>
diff --git a/ssh/seahorse-ssh-upload.ui b/ssh/seahorse-ssh-upload.ui
index 92c086b..41e8442 100644
--- a/ssh/seahorse-ssh-upload.ui
+++ b/ssh/seahorse-ssh-upload.ui
@@ -2,209 +2,146 @@
<interface>
<requires lib="gtk+" version="2.16"/>
<!-- interface-naming-policy toplevel-contextual -->
- <object class="GtkImage" id="setup-image">
- <property name="stock">gtk-go-up</property>
- </object>
- <object class="GtkDialog" id="ssh-upload">
- <property name="border_width">5</property>
- <property name="title" translatable="yes">Set Up Computer for SSH Connection</property>
- <property name="resizable">False</property>
- <property name="modal">True</property>
- <property name="window_position">center-on-parent</property>
- <property name="default_width">400</property>
- <property name="type_hint">dialog</property>
- <property name="skip_taskbar_hint">True</property>
- <property name="skip_pager_hint">True</property>
- <child internal-child="vbox">
- <object class="GtkVBox" id="dialog-vbox1">
+ <object class="GtkBox" id="ssh-upload">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkBox" id="vbox1">
<property name="visible">True</property>
- <property name="spacing">2</property>
+ <property name="orientation">vertical</property>
+ <property name="border_width">5</property>
+ <property name="spacing">12</property>
<child>
- <object class="GtkVBox" id="vbox1">
+ <object class="GtkBox" id="vbox2">
<property name="visible">True</property>
- <property name="border_width">5</property>
+ <property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
- <object class="GtkVBox" id="vbox2">
+ <object class="GtkLabel" id="label5">
+ <property name="width_request">380</property>
<property name="visible">True</property>
- <property name="spacing">12</property>
- <child>
- <object class="GtkLabel" id="label5">
- <property name="width_request">380</property>
- <property name="visible">True</property>
- <property name="label" translatable="yes">To use your Secure Shell key with another
computer that uses SSH, you must already have a login account on that computer.</property>
- <property name="wrap">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
+ <property name="label" translatable="yes">To use your Secure Shell key with another computer
that uses SSH, you must already have a login account on that computer.</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTable" id="table1">
+ <property name="visible">True</property>
+ <property name="n_rows">2</property>
+ <property name="n_columns">2</property>
+ <property name="column_spacing">12</property>
+ <property name="row_spacing">6</property>
<child>
- <object class="GtkTable" id="table1">
+ <object class="GtkBox" id="vbox3">
<property name="visible">True</property>
- <property name="n_rows">2</property>
- <property name="n_columns">2</property>
- <property name="column_spacing">12</property>
- <property name="row_spacing">6</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">3</property>
<child>
- <object class="GtkVBox" id="vbox3">
+ <object class="GtkEntry" id="host-entry">
<property name="visible">True</property>
- <property name="spacing">3</property>
- <child>
- <object class="GtkEntry" id="host-label">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="has_focus">True</property>
- <property name="tooltip_text" translatable="yes">The host name or address of the
server.</property>
- <property name="invisible_char">●</property>
- <property name="activates_default">True</property>
- <signal name="changed" handler="on_upload_input_changed"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="label7">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">eg:
fileserver.example.com:port</property>
- <attributes>
- <attribute name="style" value="italic"/>
- </attributes>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options">GTK_FILL</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="label3">
- <property name="visible">True</property>
- <property name="xalign">1</property>
- <property name="yalign">0</property>
- <property name="ypad">4</property>
- <property name="label" translatable="yes">_Server address:</property>
- <property name="use_underline">True</property>
- </object>
- <packing>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options">GTK_FILL</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="label2">
- <property name="visible">True</property>
- <property name="xalign">1</property>
- <property name="label" translatable="yes">_Login name:</property>
- <property name="use_underline">True</property>
- <property name="mnemonic_widget">user-label</property>
+ <property name="can_focus">True</property>
+ <property name="has_focus">True</property>
+ <property name="tooltip_text" translatable="yes">The host name or address of the
server.</property>
+ <property name="invisible_char">●</property>
+ <property name="activates_default">True</property>
</object>
<packing>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options"></property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
</packing>
</child>
<child>
- <object class="GtkEntry" id="user-label">
+ <object class="GtkLabel" id="label7">
<property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="has_focus">True</property>
- <property name="tooltip_text" translatable="yes">The host name or address of the
server.</property>
- <property name="invisible_char">●</property>
- <property name="activates_default">True</property>
- <signal name="changed" handler="on_upload_input_changed"/>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">eg: fileserver.example.com:port</property>
+ <attributes>
+ <attribute name="style" value="italic"/>
+ </attributes>
</object>
<packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- <property name="top_attach">1</property>
- <property name="bottom_attach">2</property>
- <property name="y_options"></property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
</packing>
</child>
</object>
<packing>
- <property name="position">1</property>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="yalign">0</property>
+ <property name="ypad">4</property>
+ <property name="label" translatable="yes">_Server address:</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Login name:</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">user-entry</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="user-entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="has_focus">True</property>
+ <property name="tooltip_text" translatable="yes">The host name or address of the
server.</property>
+ <property name="invisible_char">●</property>
+ <property name="activates_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options"></property>
</packing>
</child>
</object>
<packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child internal-child="action_area">
- <object class="GtkHButtonBox" id="dialog-action_area1">
- <property name="visible">True</property>
- <property name="layout_style">end</property>
- <child>
- <object class="GtkButton" id="cancel">
- <property name="label">gtk-cancel</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="can_default">True</property>
- <property name="receives_default">False</property>
- <property name="use_stock">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="ok">
- <property name="label" translatable="yes">_Set Up</property>
- <property name="visible">True</property>
- <property name="sensitive">False</property>
- <property name="can_focus">True</property>
- <property name="has_focus">True</property>
- <property name="can_default">True</property>
- <property name="has_default">True</property>
- <property name="receives_default">False</property>
- <property name="image">setup-image</property>
- <property name="use_underline">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
- <property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
</object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">1</property>
+ </packing>
</child>
- <action-widgets>
- <action-widget response="-6">cancel</action-widget>
- <action-widget response="-3">ok</action-widget>
- </action-widgets>
</object>
</interface>
diff --git a/ssh/source.vala b/ssh/source.vala
new file mode 100644
index 0000000..f29df29
--- /dev/null
+++ b/ssh/source.vala
@@ -0,0 +1,462 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2004-2006 Stefan Walter
+ * Copyright (C) 2011 Collabora Ltd.
+ * Copyright (c) 2016 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * The {@link Place} where SSH keys are stored. By default that is ~/.ssh.
+ * Basically, this becomes an interface to the SSH home directory.
+ */
+public class Seahorse.Ssh.Source : GLib.Object, Gcr.Collection, Seahorse.Place {
+
+ public const string AUTHORIZED_KEYS_FILE = "authorized_keys";
+ public const string OTHER_KEYS_FILE = "other_keys.seahorse";
+
+ // The home directory for SSH keys.
+ private string ssh_homedir;
+ // Source for refresh timeout
+ private uint scheduled_refresh_source;
+ // For monitoring the .ssh directory
+ private FileMonitor monitor_handle;
+
+ // Maps filenames (of the public parts) to keys
+ private HashTable<string, Key> keys;
+
+ public string label {
+ owned get { return _("OpenSSH keys"); }
+ set { }
+ }
+
+ public string description {
+ owned get { return _("OpenSSH: %s").printf(this.ssh_homedir); }
+ }
+
+ public string uri {
+ owned get { return _("openssh://%s").printf(this.ssh_homedir); }
+ }
+
+ public Icon icon {
+ owned get { return new ThemedIcon(Gcr.ICON_HOME_DIRECTORY); }
+ }
+
+ public Gtk.ActionGroup? actions {
+ owned get { return null; }
+ }
+
+ /**
+ * The directory containing the SSH keys.
+ */
+ public string base_directory {
+ get { return ssh_homedir; }
+ }
+
+ construct {
+ this.keys = new HashTable<string, Key>(str_hash, str_equal);
+
+ this.scheduled_refresh_source = 0;
+ this.monitor_handle = null;
+
+ this.ssh_homedir = "%s/.ssh".printf(Environment.get_home_dir());
+
+ // Make the .ssh directory if it doesn't exist
+ if (!FileUtils.test(this.ssh_homedir, FileTest.EXISTS)) {
+ if (DirUtils.create(this.ssh_homedir, 0700) != 0) {
+ warning("couldn't create .ssh directory: %s", this.ssh_homedir);
+ return;
+ }
+ }
+
+ monitor_ssh_homedir();
+ }
+
+ private bool check_file_for_ssh(string filename) {
+ if(!FileUtils.test(filename, FileTest.IS_REGULAR))
+ return false;
+
+ try {
+ string contents;
+ FileUtils.get_contents(filename, out contents);
+
+ // Check for our signature
+ return " PRIVATE KEY-----" in contents;
+ } catch (FileError e) {
+ warning("Error reading file '%s' to check for SSH key. %s".printf(filename, e.message));
+ }
+
+ return false;
+ }
+
+ private void cancel_scheduled_refresh () {
+ if (this.scheduled_refresh_source != 0) {
+ debug("Cancelling scheduled refresh event");
+ GLib.Source.remove(this.scheduled_refresh_source);
+ this.scheduled_refresh_source = 0;
+ }
+ }
+
+ private bool scheduled_refresh() {
+ debug("Scheduled refresh event ocurring now");
+ cancel_scheduled_refresh();
+ load.begin(null);
+ return false; // don't run again
+ }
+
+ private bool scheduled_dummy() {
+ debug("Dummy refresh event occurring now");
+ this.scheduled_refresh_source = 0;
+ return false; // don't run again
+ }
+
+ private void monitor_ssh_homedir() {
+ File? dot_ssh_dir = File.new_for_path(this.ssh_homedir);
+ if (dot_ssh_dir == null)
+ return;
+
+ try {
+ this.monitor_handle = dot_ssh_dir.monitor_directory(FileMonitorFlags.NONE, null);
+ this.monitor_handle.changed.connect((file, other_file, event_type) => {
+ if (this.scheduled_refresh_source != 0 ||
+ (event_type != FileMonitorEvent.CHANGED &&
+ event_type != FileMonitorEvent.CHANGES_DONE_HINT &&
+ event_type != FileMonitorEvent.DELETED &&
+ event_type != FileMonitorEvent.CREATED))
+ return;
+
+ string? path = file.get_path();
+ if (path == null)
+ return;
+
+ // Filter out any noise
+ if (event_type != FileMonitorEvent.DELETED
+ && !path.has_suffix(AUTHORIZED_KEYS_FILE)
+ && !path.has_suffix(OTHER_KEYS_FILE)
+ && !path.has_suffix(".pub")
+ && !SecData.contains_private_key(path))
+ return;
+
+ debug("Scheduling refresh event due to file changes");
+ this.scheduled_refresh_source = Timeout.add(500, scheduled_refresh);
+ });
+ } catch (GLib.Error e) {
+ warning("couldn't monitor ssh directory: %s: %s", this.ssh_homedir, e.message);
+ }
+ }
+
+ public uint get_length() {
+ return this.keys.size();
+ }
+
+ public List<weak GLib.Object> get_objects() {
+ return this.keys.get_values();
+ }
+
+ public bool contains(GLib.Object object) {
+ Key? key = (object as Key);
+ if (key == null)
+ return false;
+
+ string filename = key.get_location();
+ return this.keys.lookup(filename) == object;
+ }
+
+ public void remove_object(GLib.Object object) {
+ Key? key = object as Key;
+ if (key == null)
+ return;
+
+ string? filename = key.get_location();
+ if (filename == null || this.keys.lookup(filename) != key)
+ return;
+
+ this.keys.remove(filename);
+ removed(key);
+ }
+
+ public string file_for_public(bool authorized) {
+ return Path.build_filename(this.ssh_homedir,
+ authorized ? AUTHORIZED_KEYS_FILE : OTHER_KEYS_FILE);
+ }
+
+ public async Key? add_key_from_filename(string? privfile) throws GLib.Error {
+ if (privfile == null)
+ return null;
+
+ // Check if it was already loaded once. If not, load it now
+ Key? key = this.keys.lookup(privfile);
+ if (key != null)
+ return key;
+
+ return yield load_key_for_private_file(privfile);
+ }
+
+ // Loads the (public) key for a private key.
+ private async Key? load_key_for_private_file(string privfile) throws GLib.Error {
+ string pubfile = privfile + ".pub";
+ Source src = this;
+ Key? key = null;
+
+ // possibly an SSH key?
+ if (FileUtils.test(privfile, FileTest.EXISTS)
+ && FileUtils.test(pubfile, FileTest.EXISTS)
+ && check_file_for_ssh(privfile)) {
+ try {
+ yield Key.parse_file(pubfile, (keydata) => {
+ key = Source.add_key_from_parsed_data(src, keydata, pubfile, false, false, privfile);
+ return true;
+ });
+ } catch (GLib.Error e) {
+ throw new Error.GENERAL("Couldn't read SSH file: %s (%s)".printf(pubfile, e.message));
+ }
+ }
+
+ return key;
+ }
+
+ public string? export_private(Key key) throws GLib.Error {
+ KeyData? keydata = key.key_data;
+ if (keydata == null)
+ return null;
+
+ if (keydata.privfile == null)
+ throw new Error.GENERAL(_("No private key file is available for this key."));
+
+ // And then the data itself
+ string results;
+ if (!FileUtils.get_contents(keydata.privfile, out results))
+ return null;
+
+ return results;
+ }
+
+ /**
+ * Loads the keys from this Source's directory.
+ *
+ * @param cancellable Use this to cancel the operation.
+ */
+ public async bool load(GLib.Cancellable? cancellable) throws GLib.Error {
+ Source src = this;
+ // Since we can find duplicate keys, limit them with this hash
+ GenericSet<string> loaded = new GenericSet<string>(str_hash, str_equal);
+ // Keys that currently exist, so we can remove any that disappeared
+ GenericSet<string> checks = load_present_keys();
+
+ // Schedule a dummy refresh. This blocks all monitoring for a while
+ cancel_scheduled_refresh();
+ this.scheduled_refresh_source = Timeout.add(500, scheduled_dummy);
+ debug("scheduled a dummy refresh");
+
+ // List the .ssh directory for private keys
+ Dir dir = Dir.open(this.ssh_homedir);
+
+ // Load each key file in ~/.ssh
+ string? filename = null;
+ while ((filename = dir.read_name()) != null) {
+ string privfile = Path.build_filename(this.ssh_homedir, filename);
+ load_key_for_private_file.begin(privfile);
+ }
+
+ // Now load the authorized keys file
+ string pubfile = file_for_public(true);
+ Key.parse_file.begin(pubfile, (keydata) => {
+ Source.add_key_from_parsed_data(src, keydata, pubfile, true, true, null, checks, loaded);
+ return true;
+ });
+
+ // Load the other keys file
+ pubfile = file_for_public(false);
+ Key.parse_file.begin(pubfile, (keydata) => {
+ Source.add_key_from_parsed_data(src, keydata, pubfile, true, false, null, checks, loaded);
+ return true;
+ });
+
+ return true;
+ }
+
+ private GenericSet<string> load_present_keys() {
+ GenericSet<string> checks = new GenericSet<string>(str_hash, str_equal);
+ this.keys.foreach((filename, key) => checks.add(filename));
+ return checks;
+ }
+
+ public static Key? add_key_from_parsed_data(Source src,
+ KeyData? keydata,
+ string pubfile,
+ bool partial,
+ bool authorized,
+ string? privfile = null,
+ GenericSet<string>? checks = null,
+ GenericSet<string>? loaded = null) {
+ if (keydata == null || !keydata.is_valid())
+ return null;
+
+ keydata.pubfile = pubfile;
+ keydata.partial = partial;
+ keydata.authorized = authorized;
+ if (privfile != null)
+ keydata.privfile = privfile;
+
+ string? location = keydata.get_location();
+ if (location == null)
+ return null;
+
+ // Does src key exist in the context?
+ Key? prev = src.keys.lookup(location);
+
+ // Mark src key as seen
+ if (checks != null)
+ checks.remove(location);
+
+ // See if we've already gotten a key like src in this load batch
+ if (loaded != null) {
+ if (location in loaded) {
+ if (prev != null)
+ prev.merge_keydata(keydata);
+ return null;
+ }
+
+ // Mark src key as loaded
+ loaded.add(location);
+ }
+
+ // If we already have src key then just transfer ownership of keydata
+ if (prev != null) {
+ prev.key_data = keydata;
+ return prev;
+ }
+
+ // Create a new key
+ Key key = new Key(src, keydata);
+ src.keys.insert(location, key);
+ src.added(key);
+
+ return key;
+ }
+
+
+ /**
+ * Parse an inputstream into a list of keys and import those keys.
+ */
+ public async List<Key>? import_async(InputStream input, Gtk.Window transient_for,
+ Cancellable? cancellable = null) throws GLib.Error {
+ uint8[] buffer = new uint8[4096];
+ size_t bytes_read;
+ input.read_all(buffer, out bytes_read, cancellable);
+
+ string fullpath = file_for_public(false);
+ Source src = this;
+
+ Key.parse.begin((string) buffer,
+ (keydata) => {
+ import_public_async.begin(keydata, fullpath, cancellable);
+ return true;
+ },
+ (secdata) => {
+ new PrivateImportOperation().import_private_async.begin(src, secdata, null,
cancellable);
+ return true;
+ });
+
+ // TODO: The list of keys imported?
+ return null;
+ }
+
+ /**
+ * Import a public key into a file.
+ *
+ * @param data The public key that needs to be imported.
+ * @param filename The name of the file in which the key should be imported.
+ * @param cancellable Allows the operation to be cancelled.
+ */
+ public async string import_public_async(KeyData data,
+ string filename,
+ Cancellable? cancellable) throws GLib.Error {
+ if (data == null || !data.is_valid() || data.rawdata == null)
+ throw new Error.GENERAL("Trying to import an invalid public key.");
+
+ KeyData.filter_file(filename, data);
+
+ return data.fingerprint;
+ }
+
+ /**
+ * Returns a possible filename for a key.
+ *
+ * @param algo The type of the key
+ */
+ public string? new_filename_for_algorithm(Algorithm algo) {
+ string type = algo.to_string() ?? "unk";
+ string basename = "id_%s".printf(type.down());
+
+ string? filename = null;
+ for (int i = 0; i < int.MAX; i++) {
+ string t = (i == 0) ? basename : "%s.%d".printf(basename, i);
+ filename = Path.build_filename(this.base_directory, t);
+
+ if (!FileUtils.test(filename, FileTest.EXISTS))
+ break;
+
+ filename = null;
+ }
+
+ return filename;
+ }
+
+ /**
+ * Adds/Removes a public key to/from the authorized keys.
+ *
+ * @param key The key that needs to be authorized.
+ * @param authorize Whether the specified key needs to be (de-)authorized.
+ */
+ public async void authorize_async(Key key, bool authorize) throws GLib.Error {
+ SourceFunc callback = authorize_async.callback;
+ GLib.Error? err = null;
+
+ Thread<void*> thread = new Thread<void*>("authorize-async", () => {
+ try {
+ KeyData? keydata = key.key_data;
+ if (keydata == null)
+ throw new Error.GENERAL("Can't authorize empty key.");
+
+ if (!authorize) {
+ // Take it out of the file for authorized keys
+ string from = file_for_public(true);
+ KeyData.filter_file(from, null, keydata);
+ }
+
+ // Put it into the correct file
+ string to = file_for_public(authorize);
+ KeyData.filter_file(to, keydata, null);
+
+ // Just reload that one key
+ key.refresh();
+ } catch (GLib.Error e) {
+ err = e;
+ }
+
+ Idle.add((owned)callback);
+ return null;
+ });
+
+ yield;
+
+ thread.join();
+
+ if (err != null)
+ throw err;
+ }
+}
diff --git a/ssh/seahorse-ssh.c b/ssh/ssh.vala
similarity index 84%
rename from ssh/seahorse-ssh.c
rename to ssh/ssh.vala
index 5bb06ea..5dc3cab 100644
--- a/ssh/seahorse-ssh.c
+++ b/ssh/ssh.vala
@@ -1,33 +1,26 @@
-/*
+/*
* Seahorse
- *
+ *
* Copyright (C) 2008 Stefan Walter
- *
+ * Copyright (C) 2016 Niels De Graef
+ *
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
- *
+ *
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, see
* <http://www.gnu.org/licenses/>.
*/
-#include "seahorse-ssh.h"
-
-#include <stdlib.h>
-#include <string.h>
-
-
-
-
-
-
-
+namespace Seahorse.Ssh {
+public const string SEAHORSE_SSH_NAME = "openssh";
+}
diff --git a/ssh/upload.vala b/ssh/upload.vala
new file mode 100644
index 0000000..67e311a
--- /dev/null
+++ b/ssh/upload.vala
@@ -0,0 +1,144 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2005 Stefan Walter
+ * Copyright (C) 2011 Collabora Ltd.
+ * Copyright (C) 2016 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+public class Seahorse.Ssh.Upload : Gtk.Dialog {
+
+ private unowned List<Key> keys;
+
+ private Gtk.Entry user_entry;
+ private Gtk.Entry host_entry;
+ private Gtk.Button ok_button;
+
+ public Upload(List<Key> keys, Gtk.Window? parent) {
+ GLib.Object(border_width: 5,
+ title: _("Set Up Computer for SSH Connection"),
+ resizable: false,
+ default_width: 400,
+ skip_taskbar_hint: true,
+ skip_pager_hint: true,
+ window_position: Gtk.WindowPosition.CENTER_ON_PARENT,
+ modal: true,
+ transient_for: parent);
+
+ this.keys = keys;
+
+ load_ui();
+
+ // Default to the users current name
+ this.user_entry.text = Environment.get_user_name();
+ // Focus the host
+ this.host_entry.grab_focus();
+
+ on_upload_input_changed();
+ }
+
+ // FIXME: normally we would do this using GtkTemplate, but this is quite hard with the current build
setup
+ private void load_ui() {
+ Gtk.Builder builder = new Gtk.Builder();
+ try {
+ string path = "/org/gnome/Seahorse/seahorse-ssh-upload.ui";
+ builder.add_from_resource(path);
+ } catch (GLib.Error err) {
+ GLib.critical("%s", err.message);
+ }
+ Gtk.Container content = (Gtk.Container) builder.get_object("ssh-upload");
+ ((Gtk.Container)this.get_content_area()).add(content);
+
+ this.host_entry = (Gtk.Entry) builder.get_object("host-entry");
+ this.host_entry.changed.connect(on_upload_input_changed);
+ this.user_entry = (Gtk.Entry) builder.get_object("user-entry");
+ this.user_entry.changed.connect(on_upload_input_changed);
+
+ this.ok_button = (Gtk.Button) add_button(_("Set up"), Gtk.ResponseType.OK);
+ }
+
+ private void upload_keys() {
+ string user = this.user_entry.text.strip();
+ string host_port = this.host_entry.text.strip();
+
+ if (!user.validate() || host_port == "" || !host_port.validate())
+ return;
+
+ // Port is anything past a colon
+ string[] host_port_split = host_port.split(":", 2);
+ string host = host_port_split[0];
+ string? port = (host_port_split.length == 2)? host_port_split[1] : null;
+
+ Cancellable cancellable = new Cancellable();
+
+ // Start the upload process
+ UploadOperation op = new UploadOperation();
+ op.upload_async.begin(keys, user, host, port, cancellable, (obj, res) => {
+ try {
+ op.upload_async.end(res);
+ } catch (GLib.Error e) {
+ Seahorse.Util.show_error(this, _("Couldn’t configure Secure Shell keys on remote
computer."), e.message);
+ }
+ });
+
+ Seahorse.Progress.show(cancellable, _("Configuring Secure Shell Keys…"), false);
+ }
+
+ private void on_upload_input_changed () {
+ string user = this.user_entry.text;
+ string host = this.host_entry.text;
+
+ if (!user.validate() || !host.validate())
+ return;
+
+ // Take off port if necessary
+ int port_pos = host.index_of_char(':');
+ if (port_pos > 0) {
+ host = host.substring(0, port_pos);
+ }
+
+ this.ok_button.sensitive = (host.strip() != "") && (user.strip() != "");
+ }
+
+ /**
+ * Prompt a dialog to upload keys.
+ *
+ * @param keys The set of SSH keys that should be uploaded
+ */
+ public static void prompt(List<Key>? keys, Gtk.Window? parent) {
+ if (keys == null)
+ return;
+
+ Upload upload_dialog = new Upload(keys, parent);
+ for (;;) {
+ switch (upload_dialog.run()) {
+ case Gtk.ResponseType.HELP:
+ /* TODO: Help */
+ continue;
+ case Gtk.ResponseType.ACCEPT:
+ upload_dialog.upload_keys();
+ break;
+ default:
+ break;
+ };
+
+ break;
+ }
+
+ upload_dialog.destroy();
+ }
+
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]