Commits

Tim Freund committed 0c959f1

Create a file based keyring that can be unlocked with authorized SSH keys via an available SSH agent connection

  • Participants
  • Parent commits 2b13127

Comments (0)

Files changed (1)

keyring/backend.py

 Created by Kang Zhang on 2009-07-09
 """
 
+import base64
+import hashlib
 import os
 import sys
 import ConfigParser
         return "crypted_pass.cfg"
 
     def supported(self):
-        """Applicable for all platforms, but not recommend"
+        """Applicable for all platforms, but not recommend
         """
         try:
             from Crypto.Cipher import AES
         return status
 
     def _init_file(self):
-        """Init the password file, set the password for it.
-        """
+        """Init the password file, set the password for it."""
 
         print("Please set a password for your new keyring")
         password = None
         crypter = self._init_crypter()
         return crypter.decrypt(password_encrypted)
 
+class AgentEncryptedKeyring(BasicFileKeyring):
+    def __init__(self):
+        super(AgentEncryptedKeyring, self).__init__()
+        import Crypto, getpass
+        setattr(self, "Crypto", Crypto)
+        setattr(self, "getpass", getpass)
+        self.clear_passphrase = None
+
+        self.config = ConfigParser.RawConfigParser()
+        if os.path.exists(self.file_path):
+            self.config.read(self.file_path)
+        else:
+            self._initialize_configuration()
+
+        import paramiko
+        self.agent = paramiko.Agent()
+
+    def _hash_passphrase(self, passphrase):
+        sha = hashlib.sha256()
+        sha.update(passphrase)
+        hashed_passphrase = base64.encodestring(sha.digest()).strip()
+        return hashed_passphrase
+
+    def _initialize_configuration(self):
+        while self.clear_passphrase is None:
+            import getpass
+            passphrase = getpass.getpass("Configure a passphrase: ")
+            passphrase2 = getpass.getpass("Confirm your passphrase: ")
+            if passphrase != passphrase2:
+                print "The passphrases didn't match."
+            else:
+                self.clear_passphrase = passphrase
+                hashed_passphrase = self._hash_passphrase(passphrase)
+                self.config.add_section("keyring_credentials")
+                self.config.set("keyring_credentials",
+                                "passphrase",
+                                hashed_passphrase)
+        self.write_configuration()
+
+    def _retrieve_passphrase(self):
+        for ssh_key in self.agent.keys:
+            fingerprint = ''.join(["%02x" % ord(c) for c in ssh_key.get_fingerprint()])
+            if self.config.has_option("keyring_credentials",
+                                      fingerprint):
+                encrypted_passphrase = self.config.get("keyring_credentials",
+                                                       fingerprint)
+
+                aes_key = ssh_key.sign_ssh_data(None, ssh_key.get_fingerprint())[-32:]
+                aes = self.Crypto.Cipher.AES.new(aes_key)
+                passphrase = aes.decrypt(base64.decodestring(encrypted_passphrase))
+                passphrase = passphrase.replace('\x00', '')
+                hashed_passphrase = self._hash_passphrase(passphrase)
+
+                if hashed_passphrase != self.config.get("keyring_credentials",
+                                                        "passphrase"):
+                    print "Passphrase mismatch for key %s" % fingerprint
+                else:
+                    self.clear_passphrase = passphrase.strip()
+
+        while self.clear_passphrase is None:
+            passphrase = self.getpass.getpass("Please enter keyring passphrase: ")
+            hashed_passphrase = self._hash_passphrase(passphrase)
+            if hashed_passphrase != self.config.get("keyring_credentials",
+                                                    "passphrase"):
+                print "Passphrase mismatch"
+            else:
+                self.clear_passphrase = passphrase.strip()
+
+        self._add_authorized_keys()
+            
+    def _add_authorized_keys(self):
+        padded_passphrase = self.padded_passphrase
+
+        for ssh_key in self.agent.keys:
+            fingerprint = ''.join(["%02x" % ord(c) for c in ssh_key.get_fingerprint()])
+            if not self.config.has_option("keyring_credentials",
+                                          fingerprint):
+                print "Authorize %s to unlock this keyring? (y/N): "  % fingerprint,
+                response = sys.stdin.readline().strip()
+                if response.upper() == 'Y':
+                    aes_key = ssh_key.sign_ssh_data(None, ssh_key.get_fingerprint())[-32:]
+                    aes = self.Crypto.Cipher.AES.new(aes_key)
+                    encrypted_passphrase = base64.encodestring(aes.encrypt(padded_passphrase)).strip()
+                    self.config.set("keyring_credentials", 
+                                    fingerprint, 
+                                    encrypted_passphrase)
+                    
+                    self.write_configuration()
+                    
+
+    @property
+    def cipher(self):
+        return self.Crypto.Cipher.AES.new(self.padded_passphrase)
+
+    def decrypt(self, encrypted):
+        return self.cipher.decrypt(base64.decodestring(encrypted)).replace('\x00', '')
+
+    def encrypt(self, password):
+        return base64.encodestring(self.cipher.encrypt(self.pad_string(password)))
+
+    def filename(self):
+        return ".agent_encrypted_keyring.cfg"
+
+    def pad_string(self, to_pad):
+        if len(to_pad) > 16:
+            pad_length = (((len(to_pad) / 16) + 1) * 16) - len(to_pad)
+        else:
+            pad_length = 16 - len(to_pad)
+            
+        return to_pad + ("\x00" * pad_length)
+
+    @property
+    def padded_passphrase(self):
+        if self.passphrase is None:
+            raise Exception("Passphrase is not available")
+
+        return self.pad_string(self.passphrase)
+
+    @property
+    def passphrase(self):
+        if self.clear_passphrase is None:
+            self._retrieve_passphrase()
+        return self.clear_passphrase
+
+    def supported(self):
+        """Acceptable on any platform with an SSH Agent,
+        preferable when keys exist"""
+        try:
+            from Crypto.Cipher import AES
+        except ImportError:
+            return -1
+
+        try:
+            import paramiko
+            agent = paramiko.Agent()
+            if len(agent.keys):
+                return 1
+            else:
+                return 0
+        except ImportError:
+            return -1
+            
+    def write_configuration(self):
+        config_file = open(self.file_path, 'w')
+        self.config.write(config_file)
+
+        if config_file:
+            config_file.close()
 
 class Win32CryptoKeyring(BasicFileKeyring):
     """Win32 Cryptography Keyring"""