From 70b305711555027d1a360e896a4cc00540539ea3 Mon Sep 17 00:00:00 2001
From: Frank Sauerburger <frank@sauerburger.com>
Date: Tue, 23 Feb 2021 13:19:48 +0100
Subject: [PATCH] Add fully styled ssh directory

---
 keys/settings.py                              |  5 +-
 keys_home/static/keys_home/css/main.css       |  4 +
 keys_home/templates/keys_home/base.html       | 12 ++-
 requirements.txt                              |  1 +
 ssh/models.py                                 | 37 ++++++++
 ssh/templates/ssh/sshpublickey_create.html    |  7 +-
 .../ssh/sshpublickey_download_openssh.html    |  1 +
 .../ssh/sshpublickey_download_rfc4716.html    |  1 +
 ssh/templates/ssh/sshpublickey_list.html      | 85 +++++++++++++++++--
 ssh/urls.py                                   |  4 +
 ssh/views.py                                  | 12 +++
 11 files changed, 154 insertions(+), 15 deletions(-)
 create mode 100644 ssh/templates/ssh/sshpublickey_download_openssh.html
 create mode 100644 ssh/templates/ssh/sshpublickey_download_rfc4716.html

diff --git a/keys/settings.py b/keys/settings.py
index b49e3b5..9585f93 100644
--- a/keys/settings.py
+++ b/keys/settings.py
@@ -41,9 +41,12 @@ INSTALLED_APPS = [
     'pgp',
     'ssh',
     'guardian',
-    'fontawesome-free'
+    'fontawesome-free',
+    'crispy_forms',
 ]
 
+CRISPY_TEMPLATE_PACK = 'bootstrap4'
+
 AUTHENTICATION_BACKENDS = (
     'django.contrib.auth.backends.ModelBackend', # this is default
     'guardian.backends.ObjectPermissionBackend',
diff --git a/keys_home/static/keys_home/css/main.css b/keys_home/static/keys_home/css/main.css
index be6cb66..5eb36bf 100644
--- a/keys_home/static/keys_home/css/main.css
+++ b/keys_home/static/keys_home/css/main.css
@@ -39,3 +39,7 @@ body {
 .mainlist a:hover {
     text-decoration: none;
 }
+
+pre {
+  margin-bottom: 0;
+}
diff --git a/keys_home/templates/keys_home/base.html b/keys_home/templates/keys_home/base.html
index 364cd70..38c9a4c 100644
--- a/keys_home/templates/keys_home/base.html
+++ b/keys_home/templates/keys_home/base.html
@@ -41,9 +41,17 @@
             </a>
           </li>
           <li class="ml-4 nav-item">
-            <a class="nav-link" href="">
+            {% if request.user.is_anonymous %}
+              <a class="nav-link" href="">
+                <i class="fas fa-sign-in-alt"></i>
                 Login
-            </a>
+              </a>
+            {% else %}
+              <a class="nav-link" href="">
+                <i class="fas fa-user"></i>
+                {{ request.user.username }}
+              </a>
+            {% endif %}
           </li>
         </ul>
     </div>
diff --git a/requirements.txt b/requirements.txt
index d7786cb..bf56ccf 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
 django-guardian
 pgpy
 fontawesome-free
+django-crispy-forms
diff --git a/ssh/models.py b/ssh/models.py
index c5635ce..4a0d3d5 100644
--- a/ssh/models.py
+++ b/ssh/models.py
@@ -1,4 +1,5 @@
 from base64 import b64encode, b64decode
+import struct
 import hashlib
 from django.db import models
 
@@ -27,3 +28,39 @@ class SSHPublicKey(models.Model):
         raw_key = b64decode(self.key)
         digest = hashlib.md5(raw_key).hexdigest()
         return ":".join(a + b for a, b in zip(digest[::2], digest[1::2]))
+
+    def key_length(self):
+        decoded = b64decode(self.key)
+
+        def pop(binary):
+            if len(binary) < 4:
+                return b'', b''
+            length, = struct.unpack('>i', binary[:4])
+            content = binary[4:4 + length]
+            return content, binary[4 + length:]
+
+        algo, decoded = pop(decoded)  
+        if algo == b"ssh-rsa":
+            e, decoded = pop(decoded)  
+            n, decoded = pop(decoded)  
+            return len(n) * 8 - 8
+        elif algo == b"ssh-ed25519":
+            a, decoded = pop(decoded)  
+            return len(a) * 8
+
+        return None
+
+    def key_algo(self):
+        return self.keytype.split("-", 1)[-1].upper()
+
+    def key_openssh(self):
+        return f"{self.keytype} {self.key} {self.hostname}"
+
+    def key_rfc4716(self):
+        LINE_LEN = 70
+        lines = (self.key[i:LINE_LEN+i] for i in range(0, len(self.key), LINE_LEN))
+        lines = "\n".join(lines)
+        return "---- BEGIN SSH2 PUBLIC KEY ----\n" \
+               f"Comment: \"{self.hostname}\"\n" \
+               f"{lines}\n" \
+               "---- END SSH2 PUBLIC KEY ----\n"
diff --git a/ssh/templates/ssh/sshpublickey_create.html b/ssh/templates/ssh/sshpublickey_create.html
index 4f257bc..23f3f0a 100644
--- a/ssh/templates/ssh/sshpublickey_create.html
+++ b/ssh/templates/ssh/sshpublickey_create.html
@@ -1,11 +1,12 @@
 {% extends 'keys_home/base.html' %}
+{% load crispy_forms_tags %}
 
 {% block content %}
-<h2>Add public key</h2>
+<h2>Add SSH Public Key</h2>
 <form action="" method="post">
   {% csrf_token %}
-  {{ form.as_p }}
-  <button type="submit">Add</button>
+  {{ form|crispy }}
+  <button class="btn btn-primary" type="submit">Add public key</button>
 </form>
 
 {% endblock %}
diff --git a/ssh/templates/ssh/sshpublickey_download_openssh.html b/ssh/templates/ssh/sshpublickey_download_openssh.html
new file mode 100644
index 0000000..0e15af3
--- /dev/null
+++ b/ssh/templates/ssh/sshpublickey_download_openssh.html
@@ -0,0 +1 @@
+{{ sshpublickey.key_openssh|safe }}
diff --git a/ssh/templates/ssh/sshpublickey_download_rfc4716.html b/ssh/templates/ssh/sshpublickey_download_rfc4716.html
new file mode 100644
index 0000000..c24b941
--- /dev/null
+++ b/ssh/templates/ssh/sshpublickey_download_rfc4716.html
@@ -0,0 +1 @@
+{{ sshpublickey.key_rfc4716|safe }}
diff --git a/ssh/templates/ssh/sshpublickey_list.html b/ssh/templates/ssh/sshpublickey_list.html
index 8347f17..1a8824e 100644
--- a/ssh/templates/ssh/sshpublickey_list.html
+++ b/ssh/templates/ssh/sshpublickey_list.html
@@ -2,18 +2,85 @@
 
 {% block content %}
 
-{% if perms.ssh.add_sshpublickey %}
-<a href="{% url 'ssh-create' %}">Add public key</a>
-{% endif %}
+<h2 class="d-flex justify-content-between">
+  <div>SSH Public Keys</div>
+  {% if perms.ssh.add_sshpublickey %}
+  <a class="btn btn-outline-primary" href="{% url 'ssh-create' %}">
+    <i class="fas fa-plus"></i>
+  </a>
+  {% endif %}
+</h2>
+
 
 {% if sshpublickey_list %}
-<ul>
+<ul class="list-unstyled">
 {% for publickey in sshpublickey_list %}
-    <li>{{ publickey.server }}:<br />
-        SHA256: {{ publickey.fingerprint_sha256 }}<br /> 
-        MD5: {{ publickey.fingerprint_md5 }}<br /> 
-        <pre>{{ publickey.keytype }} {{ publickey.key }} {{ publickey.hostname }}</pre>
-    </li>
+    <li class="border my-4 rounded">
+      <h3 class="bg-dark text-light p-2 rounded-top d-lg-flex justify-content-between">
+        <div>
+        {% if publickey.client_cert %}
+          <i class="fas fa-user mx-2" title="Client key"></i> {{ publickey.server }}
+        {% else %}
+          <i class="fas fa-server mx-2" title="Host key"></i> {{ publickey.server }}
+        {% endif %}
+        </div>
+        <div style="text-muted"><small>
+          {{ publickey.key_algo }}{% if publickey.key_length %}, {{ publickey.key_length}} bits {% endif %}
+        </small></div>
+      </h3>
+      <div class="px-4 py-2 border-bottom bg-light">
+        <div class="text-muted mb-1">
+          <i class="fas fa-fingerprint mr-1"></i> Fingerprint
+          {% if not publickey.client_cert %}<small>compare on first connect</small>{% endif %}
+        </div>
+
+        <div class="input-group mb-2">
+          <input style="background-color: inherit" title="SHA256 fingerprint" type="text" class="form-control" readonly value="{{publickey.fingerprint_sha256 }}" />
+          <div class="input-group-append">
+            <div class="input-group-text">Base64 â—¦ SHA256</div>
+          </div>
+        </div>
+        <div class="input-group mb-2">
+          <input style="background-color: inherit" title="MD5 fingerprint" type="text" class="form-control" readonly value="{{publickey.fingerprint_md5 }}" />
+          <div class="input-group-append">
+            <div class="input-group-text">Hex â—¦ MD5</div>
+          </div>
+        </div>
+      </div>
+      <div class="px-4 py-2 border-bottom">
+        <div class="d-lg-flex mb-1 justify-content-between">
+          <div class="text-muted">
+            <i class="fas fa-key mr-1"></i> Public key (OpenSSH)
+            {% if publickey.client_cert %}<small>use in <code>.ssh/authorized_keys</code></small>{% endif %}
+          </div>
+          <div>
+            <a href="{% url 'ssh-download-openssh' publickey.pk %}">
+              <i class="fas fa-download mr-1"></i>
+              {{ request.scheme }}://{{ request.get_host}}{% url 'ssh-download-openssh' publickey.pk %}
+            </a>
+          </div>
+        </div>
+        <pre style="white-space: normal; word-break: break-all;">
+            {{ publickey.key_openssh }}
+        </pre>
+      </div>
+      <div class="px-4 py-2">
+        <div class="d-lg-flex mb-1 justify-content-between">
+          <div class="text-muted">
+            <i class="fas fa-key mr-1"></i> Public key (RFC4716)
+            {% if publickey.client_cert %}<small>use in <code>.sftp/authorized_keys</code></small>{% endif %}
+          </div>
+
+          <div>
+            <a href="{% url 'ssh-download-rfc4716' publickey.pk %}">
+              <i class="fas fa-download mr-1"></i>
+              {{ request.scheme }}://{{ request.get_host}}{% url 'ssh-download-rfc4716' publickey.pk %}
+            </a>
+          </div>
+        </div>
+        <pre>{{ publickey.key_rfc4716 }}</pre>
+      </div>
+  </li>
 {% endfor %}
 </ul>
 {% else %}
diff --git a/ssh/urls.py b/ssh/urls.py
index 8e1257d..ce0fde0 100644
--- a/ssh/urls.py
+++ b/ssh/urls.py
@@ -6,4 +6,8 @@ from . import views
 urlpatterns = [
     path("", views.PublicKeyListView.as_view(), name="ssh-list"),
     path("new", views.publickey_create, name="ssh-create"),
+    path("openssh/<int:pk>", views.PublicKeyDownloadOpenSshView.as_view(),
+         name="ssh-download-openssh"),
+    path("rfc4716/<int:pk>", views.PublicKeyDownloadRfc4716View.as_view(),
+         name="ssh-download-rfc4716"),
 ]
diff --git a/ssh/views.py b/ssh/views.py
index 3a8cd2b..72224f6 100644
--- a/ssh/views.py
+++ b/ssh/views.py
@@ -41,3 +41,15 @@ def publickey_create(request):
     return render(request,
                   'ssh/sshpublickey_create.html',
                   {'form': form})
+
+class PublicKeyDownloadOpenSshView(PermissionRequiredMixin, DetailView):
+    model = models.SSHPublicKey
+    template_name = "ssh/sshpublickey_download_openssh.html"
+    content_type = "text/plain"
+    permission_required = ['view_sshpublickey']
+
+class PublicKeyDownloadRfc4716View(PermissionRequiredMixin, DetailView):
+    model = models.SSHPublicKey
+    template_name = "ssh/sshpublickey_download_rfc4716.html"
+    content_type = "text/plain"
+    permission_required = ['view_sshpublickey']
-- 
GitLab