FEATURE: Added mechanism for exporting keypair.

FEATURE: Added mechanism for exporting keypair.

diff --git a/assets/javascripts/discourse/connectors/user-preferences-account/encrypt.hbs b/assets/javascripts/discourse/connectors/user-preferences-account/encrypt.hbs
index 5960a6d..88a9221 100644
--- a/assets/javascripts/discourse/connectors/user-preferences-account/encrypt.hbs
+++ b/assets/javascripts/discourse/connectors/user-preferences-account/encrypt.hbs
@@ -30,6 +30,7 @@
             {{else}}
               {{d-button icon="exchange" action="showPassphraseInput" label="encrypt.preferences.change" id="change"}}
               {{d-button icon="times" action="deactivateEncrypt" label="encrypt.preferences.deactivate" id="deactivate"}}
+              {{d-button icon="file-export" action="export" label="encrypt.export.title"}}
             {{/if}}
           {{else}}
             <form>
diff --git a/assets/javascripts/discourse/connectors/user-preferences-account/encrypt.js.es6 b/assets/javascripts/discourse/connectors/user-preferences-account/encrypt.js.es6
index 08eee0f..5cd86bf 100644
--- a/assets/javascripts/discourse/connectors/user-preferences-account/encrypt.js.es6
+++ b/assets/javascripts/discourse/connectors/user-preferences-account/encrypt.js.es6
@@ -1,6 +1,7 @@
 import { ajax } from "discourse/lib/ajax";
 import { popupAjaxError } from "discourse/lib/ajax-error";
 import { registerHelper } from "discourse-common/lib/helpers";
+import showModal from "discourse/lib/show-modal";
 import {
   exportPrivateKey,
   exportPublicKey,
@@ -263,6 +264,10 @@ export default {
           isEncryptActive: false
         });
       });
+    },
+
+    export() {
+      showModal("export-keypair");
     }
   }
 };
diff --git a/assets/javascripts/discourse/controllers/export-keypair.js.es6 b/assets/javascripts/discourse/controllers/export-keypair.js.es6
new file mode 100644
index 0000000..bf39231
--- /dev/null
+++ b/assets/javascripts/discourse/controllers/export-keypair.js.es6
@@ -0,0 +1,80 @@
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+import copyText from "discourse/lib/copy-text";
+import {
+  generatePassphraseKey,
+  importPrivateKey,
+  exportPublicKey
+} from "discourse/plugins/discourse-encrypt/lib/keys";
+import {
+  PACKED_KEY_COLUMNS,
+  PACKED_KEY_HEADER,
+  PACKED_KEY_SEPARATOR,
+  PACKED_KEY_FOOTER
+} from "discourse/plugins/discourse-encrypt/lib/discourse";
+
+export default Ember.Controller.extend(ModalFunctionality, {
+  onShow() {
+    this.setProperties({
+      passphrase: "",
+      exported: "",
+      error: ""
+    });
+  },
+
+  onClose() {
+    this.onShow();
+  },
+
+  packKeyPair(publicKey, privateKey) {
+    const segments = [];
+    segments.push(PACKED_KEY_HEADER);
+    for (let i = 0, len = publicKey.length; i < len; i += PACKED_KEY_COLUMNS) {
+      segments.push(publicKey.substr(i, PACKED_KEY_COLUMNS));
+    }
+    segments.push(PACKED_KEY_SEPARATOR);
+    for (let i = 0, len = privateKey.length; i < len; i += PACKED_KEY_COLUMNS) {
+      segments.push(privateKey.substr(i, PACKED_KEY_COLUMNS));
+    }
+    segments.push(PACKED_KEY_FOOTER);
+    return segments.join("\n");
+  },
+
+  actions: {
+    export() {
+      this.set("inProgress", true);
+
+      const user = Discourse.User.current();
+      const publicStr = user.get("custom_fields.encrypt_public_key");
+      const privateStr = user.get("custom_fields.encrypt_private_key");
+      const salt = user.get("custom_fields.encrypt_salt");
+      const passphrase = this.get("passphrase");
+
+      const exportedPrivateStr = generatePassphraseKey(passphrase, salt)
+        .then(key => importPrivateKey(privateStr, key, true))
+        .then(privateKey => exportPublicKey(privateKey));
+
+      Promise.all([publicStr, exportedPrivateStr])
+        .then(([publicKey, privateKey]) => {
+          this.setProperties({
+            exported: this.packKeyPair(publicKey, privateKey),
+            inProgress: false,
+            error: ""
+          });
+        })
+        .catch(() => {
+          this.setProperties({
+            inProgress: false,
+            error: I18n.t("encrypt.preferences.passphrase_invalid")
+          });
+        });
+    },
+
+    copy() {
+      const $copyRange = $('pre.exported-keypair');
+      if (copyText("", $copyRange[0])) {
+        this.set("copied", true);
+        Ember.run.later(() => this.set("copied", false), 2000);
+      }
+    }
+  }
+});
diff --git a/assets/javascripts/discourse/templates/modal/export-keypair.hbs b/assets/javascripts/discourse/templates/modal/export-keypair.hbs
new file mode 100644
index 0000000..5764bc3
--- /dev/null
+++ b/assets/javascripts/discourse/templates/modal/export-keypair.hbs
@@ -0,0 +1,26 @@
+{{#d-modal-body title="encrypt.export.title"}}
+  {{#if error}}
+    <div class="alert alert-error">{{error}}</div>
+  {{/if}}
+  <div>
+    <p>{{i18n 'encrypt.export.instructions'}}</p>
+    {{#if exported}}
+      <pre class="exported-keypair">{{exported}}</pre>
+    {{else}}
+      <label for="">{{i18n "encrypt.preferences.passphrase_label"}}</label>
+      {{input type="password" value=passphrase id="passphrase" autocomplete="current-password" disabled=inProgress}}
+    {{/if}}
+  </div>
+{{/d-modal-body}}
+
+<div class="modal-footer">
+  {{d-button class="btn btn-primary" icon="file-export" label="encrypt.export.title" action="export"}}
+  {{#if exported}}
+    {{#if copied}}
+      {{d-button class="btn" icon="clipboard" label="admin.customize.copied_to_clipboard" action="copy"}}
+    {{else}}
+      {{d-button class="btn" icon="clipboard" label="admin.customize.copy_to_clipboard" action="copy"}}
+    {{/if}}
+  {{/if}}
+  {{d-modal-cancel close=(action "closeModal")}}
+</div>
diff --git a/assets/javascripts/lib/discourse.js.es6 b/assets/javascripts/lib/discourse.js.es6
index e9a475a..5bbd6dc 100644
--- a/assets/javascripts/lib/discourse.js.es6
+++ b/assets/javascripts/lib/discourse.js.es6
@@ -17,6 +17,17 @@ export const ENCRYPT_ENABLED = 1;
 export const ENCRYPT_ACTIVE = 2;
 
 /**
+ * Useful variables for key import and export format.
+ */
+export const PACKED_KEY_COLUMNS = 71;
+export const PACKED_KEY_HEADER =
+  "============== BEGIN EXPORTED DISCOURSE ENCRYPT KEY PAIR ==============";
+export const PACKED_KEY_SEPARATOR =
+  "-----------------------------------------------------------------------";
+export const PACKED_KEY_FOOTER =
+  "=============== END EXPORTED DISCOURSE ENCRYPT KEY PAIR ===============";
+
+/**
  * @var User's public key used to encrypt topic keys and drafts for private message.
  */
 let publicKey;
diff --git a/assets/stylesheets/common/encrypt.scss b/assets/stylesheets/common/encrypt.scss
index c00aab2..a73643c 100644
--- a/assets/stylesheets/common/encrypt.scss
+++ b/assets/stylesheets/common/encrypt.scss
@@ -1,3 +1,8 @@
+pre.exported-keypair {
+  font-size: 0.75rem;
+  max-height: 300px;
+}
+
 #reply-control {
   .reply-to {
     .d-icon {
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index c127720..dce9930 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -43,3 +43,7 @@ en:
         deactivate: "Deactive Encryption on this Device"
         enable: "Enable Encrypted Messages"
         change: "Change Passphrase"
+
+      export:
+        title: "Export Encryption Keypair"
+        instructions: "You may export your keypair for safe keeping in case you forget your passphrase. Please note that the exported keypair is unprotected."
diff --git a/plugin.rb b/plugin.rb
index 7a28dfb..36f3508 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -8,7 +8,7 @@ enabled_site_setting :encrypt_enabled
 
 # Register custom stylesheet.
 register_asset "stylesheets/common/encrypt.scss"
-[ "exchange", "lock", "times", "unlock" ].each { |i| register_svg_icon i }
+[ "exchange",  "file-export", "lock", "times", "unlock" ].each { |i| register_svg_icon i }
 
 # Register custom user fields to store user's key pair (public and private key)
 # and passphrase salt.

GitHub sha: 83616911