diff options
-rw-r--r-- | README | 92 | ||||
-rwxr-xr-x | demo.py | 13 | ||||
-rwxr-xr-x | demo_server.py | 2 | ||||
-rw-r--r-- | paramiko/__init__.py | 112 | ||||
-rw-r--r-- | paramiko/auth_transport.py | 20 | ||||
-rw-r--r-- | paramiko/ber.py | 46 | ||||
-rw-r--r-- | paramiko/channel.py | 141 | ||||
-rw-r--r-- | paramiko/dsskey.py | 28 | ||||
-rw-r--r-- | paramiko/pkey.py | 97 | ||||
-rw-r--r-- | paramiko/primes.py | 24 | ||||
-rw-r--r-- | paramiko/rsakey.py | 31 | ||||
-rw-r--r-- | paramiko/ssh_exception.py | 16 | ||||
-rw-r--r-- | paramiko/transport.py | 10 | ||||
-rw-r--r-- | paramiko/util.py | 38 |
14 files changed, 456 insertions, 214 deletions
@@ -17,22 +17,20 @@ telnet and rsh for secure access to remote shells, but the protocol also includes the ability to open arbitrary channels to remote services across the encrypted tunnel (this is how sftp works, for example). -the module works by taking a socket-like object that you pass in, negotiating -with the remote server, authenticating (using a password or a given private -key), and opening flow-controled "channels" to the server, which are returned -as socket-like objects. you are responsible for verifying that the server's -host key is the one you expected to see, and you have control over which kinds -of encryption or hashing you prefer (if you care), but all of the heavy lifting -is done by the paramiko module. - it is written entirely in python (no C or platform-dependent code) and is released under the GNU LGPL (lesser GPL). +the package and its API is fairly well documented in the "doc/" folder that +should have come with this archive. + *** REQUIREMENTS python 2.3 <http://www.python.org/> -pyCrypto <http://www.amk.ca/python/code/crypto.html> +pyCrypt <http://www.amk.ca/python/code/crypto.html> + +PyCrypt compiled for Win32 can be downloaded from the HashTar homepage: + http://nitace.bsd.uchicago.edu:8080/hashtar *** PORTABILITY @@ -45,12 +43,8 @@ run into Windows problems, send me a patch: portability is important to me. the Channel object supports a "fileno()" call so that it can be passed into select or poll, for polling on posix. once you call "fileno()" on a Channel, it changes behavior in some fundamental ways, and these ways require posix. -so don't call "fileno()" on a Channel on Windows. (the problem is that pipes -are used to simulate an open socket, so that the ssh "socket" has an OS-level -file descriptor. i haven't figured out how to make pipes on Windows go into -non-blocking mode yet. [if you don't understand this last sentence, don't -be afraid. the point is to make the API simple enough that you don't HAVE to -know these screwy steps. i just don't understand windows enough.]) +so don't call "fileno()" on a Channel on Windows. this is detailed in the +documentation for the "fileno" method. *** DEMO @@ -63,13 +57,15 @@ you can run demo.py with no arguments, or you can give a hostname (or username@hostname) on the command line. if you don't, it'll prompt you for a hostname and username. if you have an ".ssh/" folder, it will try to read the host keys from there, though it's easily confused. you can choose to -authenticate with a password, or with an RSA or DSS key, but it can only -read your private key file(s) if they're not password-protected. +authenticate with a password, or with an RSA or DSS key. the demo app leaves a logfile called "demo.log" so you can see what paramiko logs as it works. but the most interesting part is probably the code itself, which hopefully demonstrates how you can use the paramiko library. +a simpler example is in demo_simple.py, which is a copy of the demo client +that uses the simpler "connect" method call (new with 0.9-doduo). + there's also now a demo server (demo_server.py) which listens on port 2200 and accepts a login (robey/foo) and pretends to be a BBS, just to demonstrate how to perform the server side of things. @@ -77,63 +73,17 @@ how to perform the server side of things. *** USE -(this section could probably be improved a lot.) - -first, create a Transport by passing in an existing socket (connected to the -desired server). call "start_client(event)", passing in an event which will -be triggered when the negotiation is finished (either successfully or not). -the event is required because each new Transport creates a new worker thread -to handle incoming data asynchronously. - -after the event triggers, use "is_active()" to determine if the Transport was -successfully connected. if so, you should check the server's host key to make -sure it's what you expected. don't worry, i don't mean "check" in any crypto -sense: i mean compare the key, byte for byte, with what you saw last time, to -make sure it's the same key. Transport will handle verifying that the server's -key works. - -next, authenticate, using either "auth_key" or "auth_password". in the future, -this API may change to accomodate servers that require both forms of auth. -pass another event in so you can determine when the authentication dance is -over. if it was successful, "is_authenticated()" will return true. - -once authentication is successful, the Transport is ready to use. call -"open_channel" or "open_session" to create new channels over the Transport -(SSH2 supports many different channels over the same connection). these calls -block until they succeed or fail, and return a Channel object on success, or -None on failure. Channel objects can be treated as "socket-like objects": they -implement: - recv(nbytes) - send(data) - settimeout(timeout_in_seconds) - close() - fileno() [* see note below] -because SSH2 has a windowing kind of flow control, if you stop reading data -from a Channel and its buffer fills up, the server will be unable to send you -any more data until you read some of it. (this won't affect other channels on -the Transport, though.) - -* NOTE that if you use "fileno()", the behavior of the Channel will change -slightly, underneath. this shouldn't be noticable outside the library, but -this alternate implementation will not work on non-posix systems. so don't -try calling "fileno()" on Windows! this has the side effect that you can't -pass a Channel to "select" or "poll" on Windows (which should be fine, since -those calls don't exist on Windows). calling "fileno()" creates an OS-level -pipe and generates a real file descriptor which can be used for polling, BUT -should not be used for reading data from the channel: use "recv" instead. - -because each Transport has a worker thread running in the background, you -must call "close()" on the Transport to kill this thread. on many platforms, -the python interpreter will refuse to exit cleanly if any of these threads -are still running (and you'll have to kill -9 from another shell window). - -[fixme: add info about server mode] +the demo clients (demo.py & demo_simple.py) and the demo server +(demo_server.py) are probably the best example of how to use this package. +there is also a lot of documentation, generated with epydoc, in the doc/ +folder. point your browser there. seriously, do it. mad props to epydoc, +which actually motivated me to write more documentation than i ever would +have before. *** MISSING LINKS * ctr forms of ciphers are missing (blowfish-ctr, aes128-ctr, aes256-ctr) -* can't handle password-protected private key files * multi-part auth not supported (ie, need username AND pk) -* server mode needs better doc - +* server mode needs better documentation +* sftp? @@ -39,6 +39,7 @@ if len(l.handlers) == 0: lh.setFormatter(logging.Formatter('%(levelname)-.3s [%(asctime)s] %(name)s: %(message)s', '%Y%m%d:%H%M%S')) l.addHandler(lh) + username = '' if len(sys.argv) > 1: hostname = sys.argv[1] @@ -107,7 +108,11 @@ try: path = raw_input('RSA key [%s]: ' % default_path) if len(path) == 0: path = default_path - key.read_private_key_file(path) + try: + key.read_private_key_file(path) + except paramiko.PasswordRequiredException: + password = getpass.getpass('RSA key password: ') + key.read_private_key_file(path, password) t.auth_publickey(username, key, event) elif auth == 'd': key = paramiko.DSSKey() @@ -115,7 +120,11 @@ try: path = raw_input('DSS key [%s]: ' % default_path) if len(path) == 0: path = default_path - key.read_private_key_file(path) + try: + key.read_private_key_file(path) + except paramiko.PasswordRequiredException: + password = getpass.getpass('DSS key password: ') + key.read_private_key_file(path, password) t.auth_key(username, key, event) else: pw = getpass.getpass('Password for %s@%s: ' % (username, hostname)) diff --git a/demo_server.py b/demo_server.py index a80b7f9e..6447d4a0 100755 --- a/demo_server.py +++ b/demo_server.py @@ -37,6 +37,7 @@ class ServerTransport(paramiko.Transport): return self.AUTH_FAILED def check_auth_publickey(self, username, key): + print 'Auth attempt with key: ' + paramiko.util.hexify(key.get_fingerprint()) if (username == 'robey') and (key == self.good_pub_key): return self.AUTH_SUCCESSFUL return self.AUTH_FAILED @@ -66,6 +67,7 @@ try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('', 2200)) except Exception, e: + print '*** Bind failed: ' + str(e) traceback.print_exc() sys.exit(1) diff --git a/paramiko/__init__.py b/paramiko/__init__.py index bed1f8e8..968b250c 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -1,3 +1,40 @@ +#!/usr/bin/python + +""" +I{Paramiko} (a combination of the esperanto words for "paranoid" and "friend") +is a module for python 2.3 or greater that implements the SSH2 protocol for +secure (encrypted and authenticated) connections to remote machines. Unlike +SSL (aka TLS), the SSH2 protocol does not require heirarchical certificates +signed by a powerful central authority. You may know SSH2 as the protocol that +replaced C{telnet} and C{rsh} for secure access to remote shells, but the +protocol also includes the ability to open arbitrary channels to remote +services across an encrypted tunnel. (This is how C{sftp} works, for example.) + +To use this package, pass a socket (or socket-like object) to a L{Transport}, +and use L{start_server <paramiko.transport.BaseTransport.start_server>} or +L{start_client <paramiko.transport.BaseTransport.start_client>} to negoatite +with the remote host as either a server or client. As a client, you are +responsible for authenticating using a password or private key, and checking +the server's host key. I{(Key signature and verification is done by paramiko, +but you will need to provide private keys and check that the content of a +public key matches what you expected to see.)} As a server, you are +responsible for deciding which users, passwords, and keys to allow, and what +kind of channels to allow. + +Once you have finished, either side may request flow-controlled L{Channel}s to +the other side, which are python objects that act like sockets, but send and +receive data over the encrypted session. + +Paramiko is written entirely in python (no C or platform-dependent code) and is +released under the GNU Lesser General Public License (LGPL). + +Website: U{http://www.lag.net/~robey/paramiko/} + +@version: 0.9 (doduo) +@author: Robey Pointer +@contact: robey@lag.net +@license: GNU Lesser General Public License (LGPL) +""" import sys @@ -8,50 +45,31 @@ if (sys.version_info[0] < 2) or ((sys.version_info[0] == 2) and (sys.version_inf __author__ = "Robey Pointer <robey@lag.net>" __date__ = "10 Nov 2003" __version__ = "0.1-charmander" -__credits__ = "Huzzah!" -__license__ = "Lesser GNU Public License (LGPL)" - - -import ssh_exception, transport, auth_transport, channel, rsakey, dsskey - -class SSHException (ssh_exception.SSHException): - """ - Exception thrown by failures in SSH2 protocol negotiation or logic errors. - """ - pass - -class Transport (auth_transport.Transport): - """ - An SSH Transport attaches to a stream (usually a socket), negotiates an - encrypted session, authenticates, and then creates stream tunnels, called - L{Channel}s, across the session. Multiple channels can be multiplexed - across a single session (and often are, in the case of port forwardings). - """ - pass - -class Channel (channel.Channel): - """ - A secure tunnel across an SSH L{Transport}. A Channel is meant to behave - like a socket, and has an API that should be indistinguishable from the - python socket API. - """ - pass - -class RSAKey (rsakey.RSAKey): - """ - Representation of an RSA key which can be used to sign and verify SSH2 - data. - """ - pass - -class DSSKey (dsskey.DSSKey): - """ - Representation of a DSS key which can be used to sign an verify SSH2 - data. - """ - pass - - -__all__ = [ 'Transport', 'Channel', 'RSAKey', 'DSSKey', 'transport', - 'auth_transport', 'channel', 'rsakey', 'dsskey', 'util', - 'SSHException' ] +#__credits__ = "Huzzah!" +__license__ = "GNU Lesser General Public License (LGPL)" + + +import transport, auth_transport, channel, rsakey, dsskey, ssh_exception + +Transport = auth_transport.Transport +Channel = channel.Channel +RSAKey = rsakey.RSAKey +DSSKey = dsskey.DSSKey +SSHException = ssh_exception.SSHException +PasswordRequiredException = ssh_exception.PasswordRequiredException + + +__all__ = [ 'Transport', + 'Channel', + 'RSAKey', + 'DSSKey', + 'SSHException', + 'PasswordRequiredException', + 'transport', + 'auth_transport', + 'channel', + 'rsakey', + 'dsskey', + 'pkey', + 'ssh_exception', + 'util' ] diff --git a/paramiko/auth_transport.py b/paramiko/auth_transport.py index d2376c55..5a0cd77a 100644 --- a/paramiko/auth_transport.py +++ b/paramiko/auth_transport.py @@ -1,5 +1,10 @@ #!/usr/bin/python +""" +L{Transport} is a subclass of L{BaseTransport} that handles authentication. +This separation keeps either class file from being too unwieldy. +""" + from transport import BaseTransport from transport import _MSG_SERVICE_REQUEST, _MSG_SERVICE_ACCEPT, _MSG_USERAUTH_REQUEST, _MSG_USERAUTH_FAILURE, \ _MSG_USERAUTH_SUCCESS, _MSG_USERAUTH_BANNER @@ -11,11 +16,18 @@ _DISCONNECT_SERVICE_NOT_AVAILABLE, _DISCONNECT_AUTH_CANCELLED_BY_USER, \ _DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14 - class Transport (BaseTransport): """ - Subclass of L{BaseTransport} that handles authentication. This separation - keeps either class file from being too unwieldy. + An SSH Transport attaches to a stream (usually a socket), negotiates an + encrypted session, authenticates, and then creates stream tunnels, called + L{Channel}s, across the session. Multiple channels can be multiplexed + across a single session (and often are, in the case of port forwardings). + + @note: Because each Transport has a worker thread running in the + background, you must call L{close} on the Transport to kill this thread. + On many platforms, the python interpreter will refuse to exit cleanly if + any of these threads are still running (and you'll have to C{kill -9} from + another shell window). """ AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED = range(3) @@ -34,7 +46,7 @@ class Transport (BaseTransport): return '<paramiko.Transport (unconnected)>' out = '<paramiko.Transport' if self.local_cipher != '': - out += ' (cipher %s)' % self.local_cipher + out += ' (cipher %s, %d bits)' % (self.local_cipher, self._cipher_info[self.local_cipher]['key-size'] * 8) if self.authenticated: if len(self.channels) == 1: out += ' (active; 1 open channel)' diff --git a/paramiko/ber.py b/paramiko/ber.py index 7fe1dd09..ed7db223 100644 --- a/paramiko/ber.py +++ b/paramiko/ber.py @@ -1,45 +1,15 @@ #!/usr/bin/python -import struct +import struct, util -def inflate_long(s, always_positive=0): - "turns a normalized byte string into a long-int (adapted from Crypto.Util.number)" - out = 0L - if len(s) % 4: - filler = '\x00' - if not always_positive and (ord(s[0]) >= 0x80): - # negative - filler = '\xff' - s = filler * (4 - len(s) % 4) + s - # FIXME: this doesn't actually handle negative. - # luckily ssh never uses negative bignums. - for i in range(0, len(s), 4): - out = (out << 32) + struct.unpack('>I', s[i:i+4])[0] - return out - -def deflate_long(n, add_sign_padding=1): - "turns a long-int into a normalized byte string (adapted from Crypto.Util.number)" - # after much testing, this algorithm was deemed to be the fastest - s = '' - n = long(n) - while n > 0: - s = struct.pack('>I', n & 0xffffffffL) + s - n = n >> 32 - # strip off leading zeros - for i in enumerate(s): - if i[1] != '\000': - break - else: - # only happens when n == 0 - s = '\000' - i = (0,) - s = s[i[0]:] - if (ord(s[0]) >= 0x80) and add_sign_padding: - s = '\x00' + s - return s +class BERException (Exception): + pass class BER(object): + """ + Robey's tiny little attempt at a BER decoder. + """ def __init__(self, content=''): self.content = content @@ -95,10 +65,10 @@ class BER(object): return self.decode_sequence(data) elif id == 2: # int - return inflate_long(data) + return util.inflate_long(data) else: # 1: boolean (00 false, otherwise true) - raise Exception('Unknown ber encoding type %d (robey is lazy)' % id) + raise BERException('Unknown ber encoding type %d (robey is lazy)' % id) def decode_sequence(data): out = [] diff --git a/paramiko/channel.py b/paramiko/channel.py index 78d8ef2c..9be2fd0c 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -1,3 +1,9 @@ +#!/usr/bin/python + +""" +Abstraction for an SSH2 channel. +""" + from message import Message from ssh_exception import SSHException from transport import _MSG_CHANNEL_REQUEST, _MSG_CHANNEL_CLOSE, _MSG_CHANNEL_WINDOW_ADJUST, _MSG_CHANNEL_DATA, \ @@ -13,12 +19,32 @@ def _set_nonblocking(fd): fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK) -class Channel(object): +class Channel (object): """ - Abstraction for an SSH2 channel. + A secure tunnel across an SSH L{Transport}. A Channel is meant to behave + like a socket, and has an API that should be indistinguishable from the + python socket API. + + Because SSH2 has a windowing kind of flow control, if you stop reading data + from a Channel and its buffer fills up, the server will be unable to send + you any more data until you read some of it. (This won't affect other + channels on the same transport -- all channels on a single transport are + flow-controlled independently.) Similarly, if the server isn't reading + data you send, calls to L{send} may block, unless you set a timeout. This + is exactly like a normal network socket, so it shouldn't be too surprising. """ def __init__(self, chanid): + """ + Create a new channel. The channel is not associated with any + particular session or L{Transport} until the Transport attaches it. + Normally you would only call this method from the constructor of a + subclass of L{Channel}. + + @param chanid: the ID of this channel, as passed by an existing + L{Transport}. + @type chanid: int + """ self.chanid = chanid self.transport = None self.active = 0 @@ -84,6 +110,11 @@ class Channel(object): self.transport._send_message(m) def invoke_shell(self): + """ + Request an interactive shell session on this channel. If the server + allows it, the channel will then be directly connected to the stdin + and stdout of the shell. + """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() @@ -94,6 +125,14 @@ class Channel(object): self.transport._send_message(m) def exec_command(self, command): + """ + Execute a command on the server. If the server allows it, the channel + will then be directly connected to the stdin and stdout of the command + being executed. + + @param command: a shell command to execute. + @type command: string + """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() @@ -105,6 +144,14 @@ class Channel(object): self.transport._send_message(m) def invoke_subsystem(self, subsystem): + """ + Request a subsystem on the server (for example, C{sftp}). If the + server allows it, the channel will then be directly connected to the + requested subsystem. + + @param subsystem: name of the subsystem being requested. + @type subsystem: string + """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() @@ -457,19 +504,81 @@ class Channel(object): def check_pty_request(self, term, width, height, pixelwidth, pixelheight, modes): - "override me! return True if a pty of the given dimensions (for shell access, usually) can be provided" + """ + I{(subclass override)} + Determine if a pseudo-terminal of the given dimensions (usually + requested for shell access) can be provided. + + The default implementation always returns C{False}. + + @param term: type of terminal requested (for example, C{"vt100"}). + @type term: string + @param width: width of screen in characters. + @type width: int + @param height: height of screen in characters. + @type height: int + @param pixelwidth: width of screen in pixels, if known (may be C{0} if + unknown). + @type pixelwidth: int + @param pixelheight: height of screen in pixels, if known (may be C{0} + if unknown). + @type pixelheight: int + @return: C{True} if the psuedo-terminal has been allocated; C{False} + otherwise. + @rtype: boolean + """ return False def check_shell_request(self): - "override me! return True if shell access will be provided" + """ + I{(subclass override)} + Determine if a shell will be provided to the client. If this method + returns C{True}, this channel should be connected to the stdin/stdout + of a shell. + + The default implementation always returns C{False}. + + @return: C{True} if this channel is now hooked up to a shell; C{False} + if a shell can't or won't be provided. + @rtype: boolean + """ return False def check_subsystem_request(self, name): - "override me! return True if the given subsystem can be provided" + """ + I{(subclass override)} + Determine if a requested subsystem will be provided to the client. If + this method returns C{True}, all future I/O through this channel will + be assumed to be connected to the requested subsystem. An example of + a subsystem is C{sftp}. + + The default implementation always returns C{False}. + + @return: C{True} if this channel is now hooked up to the requested + subsystem; C{False} if that subsystem can't or won't be provided. + @rtype: boolean + """ return False def check_window_change_request(self, width, height, pixelwidth, pixelheight): - "override me! return True if the pty was resized" + """ + I{(subclass override)} + Determine if the pseudo-terminal can be resized. + + The default implementation always returns C{False}. + + @param width: width of screen in characters. + @type width: int + @param height: height of screen in characters. + @type height: int + @param pixelwidth: width of screen in pixels, if known (may be C{0} if + unknown). + @type pixelwidth: int + @param pixelheight: height of screen in pixels, if known (may be C{0} + if unknown). + @type pixelheight: int + @return: C{True} if the terminal was resized; C{False} if not. + """ return False @@ -707,13 +816,16 @@ class Channel(object): self.in_window_sofar = 0 -class ChannelFile(object): +class ChannelFile (object): """ - A file-like wrapper around Channel. - Doesn't have the non-portable side effect of Channel.fileno(). - XXX Todo: the channel and its file-wrappers should be able to be closed or - garbage-collected independently, for compatibility with real sockets and - their file-wrappers. Currently, closing does nothing but flush the buffer. + A file-like wrapper around L{Channel}. A ChannelFile is created by calling + L{Channel.makefile} and doesn't have the non-portable side effect of + L{Channel.fileno}. + + @bug: To correctly emulate the file object created from a socket's + C{makefile} method, a L{Channel} and its C{ChannelFile} should be able to + be closed or garbage-collected independently. Currently, closing the + C{ChannelFile} does nothing but flush the buffer. """ def __init__(self, channel, mode = "r", buf_size = -1): @@ -740,6 +852,11 @@ class ChannelFile(object): self.softspace = False def __repr__(self): + """ + Returns a string representation of this object, for debugging. + + @rtype: string + """ return '<paramiko.ChannelFile from ' + repr(self.channel) + '>' def __iter__(self): diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index e0006e53..a7ddf69b 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -1,17 +1,23 @@ #!/usr/bin/python -import base64 +""" +L{DSSKey} +""" + from ssh_exception import SSHException from message import Message from util import inflate_long, deflate_long from Crypto.PublicKey import DSA from Crypto.Hash import SHA -from ber import BER +from ber import BER, BERException from pkey import PKey - -from util import format_binary +from ssh_exception import SSHException class DSSKey (PKey): + """ + Representation of a DSS key which can be used to sign an verify SSH2 + data. + """ def __init__(self, msg=None, data=None): self.valid = 0 @@ -84,17 +90,15 @@ class DSSKey (PKey): dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q))) return dss.verify(sigM, (sigR, sigS)) - def read_private_key_file(self, filename): + def read_private_key_file(self, filename, password=None): # private key file contains: # DSAPrivateKey = { version = 0, p, q, g, y, x } self.valid = 0 - f = open(filename, 'r') - lines = f.readlines() - f.close() - if lines[0].strip() != '-----BEGIN DSA PRIVATE KEY-----': - raise SSHException('not a valid DSA private key file') - data = base64.decodestring(''.join(lines[1:-1])) - keylist = BER(data).decode() + data = self._read_private_key_file('DSA', filename, password) + try: + keylist = BER(data).decode() + except BERException: + raise SSHException('Unable to parse key file') if (type(keylist) != type([])) or (len(keylist) < 6) or (keylist[0] != 0): raise SSHException('not a valid DSA private key file (bad ber encoding)') self.p = keylist[1] diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 6ad2845d..4d7c9a3f 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -1,12 +1,27 @@ +#!/usr/bin/python + +""" +Common API for all public keys. +""" from Crypto.Hash import MD5 +from Crypto.Cipher import DES3 from message import Message +from ssh_exception import SSHException, PasswordRequiredException +import util +import base64 class PKey (object): """ Base class for public keys. """ + # known encryption types for private key files: + _CIPHER_TABLE = { + 'DES-EDE3-CBC': { 'cipher': DES3, 'keysize': 24, 'mode': DES3.MODE_CBC } + } + + def __init__(self, msg=None, data=None): """ Create a new instance of this public key type. If C{msg} is given, @@ -101,15 +116,93 @@ class PKey (object): """ return False - def read_private_key_file(self, filename): + def read_private_key_file(self, filename, password=None): """ - Read private key contents from a file into this object. + Read private key contents from a file into this object. If the private + key is encrypted and C{password} is not C{None}, the given password + will be used to decrypt the key (otherwise L{PasswordRequiredException} + is thrown). @param filename: name of the file to read. @type filename: string + @param password: an optional password to use to decrypt the key file, + if it's encrypted. + @type password: string @raise IOError: if there was an error reading the file. + @raise PasswordRequiredException: if the private key file is + encrypted, and C{password} is C{None}. @raise SSHException: if the key file is invalid @raise binascii.Error: on base64 decoding error """ pass + + def _read_private_key_file(self, tag, filename, password=None): + """ + Read an SSH2-format private key file, looking for a string of the type + C{"BEGIN xxx PRIVATE KEY"} for some C{xxx}, base64-decode the text we + find, and return it as a string. If the private key is encrypted and + C{password} is not C{None}, the given password will be used to decrypt + the key (otherwise L{PasswordRequiredException} is thrown). + + @param tag: C{"RSA"} or C{"DSA"}, the tag used to mark the data block. + @type tag: string + @param filename: name of the file to read. + @type filename: string + @param password: an optional password to use to decrypt the key file, + if it's encrypted. + @type password: string + @return: data blob that makes up the private key. + @rtype: string + + @raise IOError: if there was an error reading the file. + @raise PasswordRequiredException: if the private key file is + encrypted, and C{password} is C{None}. + @raise SSHException: if the key file is invalid. + @raise binascii.Error: on base64 decoding error. + """ + f = open(filename, 'r') + lines = f.readlines() + f.close() + start = 0 + while (lines[start].strip() != '-----BEGIN ' + tag + ' PRIVATE KEY-----') and (start < len(lines)): + start += 1 + if start >= len(lines): + raise SSHException('not a valid ' + tag + ' private key file') + # parse any headers first + headers = {} + start += 1 + while start < len(lines): + l = lines[start].split(': ') + if len(l) == 1: + break + headers[l[0].lower()] = l[1].strip() + start += 1 + # find end + end = start + while (lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----') and (end < len(lines)): + end += 1 + # if we trudged to the end of the file, just try to cope. + data = base64.decodestring(''.join(lines[start:end])) + if not headers.has_key('proc-type'): + # unencryped: done + return data + # encrypted keyfile: will need a password + if headers['proc-type'] != '4,ENCRYPTED': + raise SSHException('Unknown private key structure "%s"' % headers['proc-type']) + try: + encryption_type, saltstr = headers['dek-info'].split(',') + except: + raise SSHException('Can\'t parse DEK-info in private key file') + if not self._CIPHER_TABLE.has_key(encryption_type): + raise SSHException('Unknown private key cipher "%s"' % encryption_type) + # if no password was passed in, raise an exception pointing out that we need one + if password is None: + raise PasswordRequiredException('Private key file is encrypted') + cipher = self._CIPHER_TABLE[encryption_type]['cipher'] + keysize = self._CIPHER_TABLE[encryption_type]['keysize'] + mode = self._CIPHER_TABLE[encryption_type]['mode'] + # this confusing line turns something like '2F91' into '/\x91' (sorry, was feeling clever) + salt = ''.join([chr(int(saltstr[i:i+2], 16)) for i in range(0, len(saltstr), 2)]) + key = util.generate_key_bytes(MD5, salt, password, keysize) + return cipher.new(key, mode, salt).decrypt(data) diff --git a/paramiko/primes.py b/paramiko/primes.py index 68e7fc19..85569ceb 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -1,28 +1,32 @@ +#!/usr/bin/python -# utility functions for dealing with primes +""" +Utility functions for dealing with primes. +""" from Crypto.Util import number -from util import bit_length, inflate_long +import util -def generate_prime(bits, randpool): +def _generate_prime(bits, randpool): + "primtive attempt at prime generation" hbyte_mask = pow(2, bits % 8) - 1 while 1: # loop catches the case where we increment n into a higher bit-range x = randpool.get_bytes((bits+7) // 8) if hbyte_mask > 0: x = chr(ord(x[0]) & hbyte_mask) + x[1:] - n = inflate_long(x, 1) + n = util.inflate_long(x, 1) n |= 1 n |= (1 << (bits - 1)) while not number.isPrime(n): n += 2 - if bit_length(n) == bits: + if util.bit_length(n) == bits: return n -def roll_random(randpool, n): +def _roll_random(randpool, n): "returns a random # from 0 to N-1" - bits = bit_length(n-1) + bits = util.bit_length(n-1) bytes = (bits + 7) // 8 hbyte_mask = pow(2, bits % 8) - 1 @@ -36,7 +40,7 @@ def roll_random(randpool, n): x = randpool.get_bytes(bytes) if hbyte_mask > 0: x = chr(ord(x[0]) & hbyte_mask) + x[1:] - num = inflate_long(x, 1) + num = util.inflate_long(x, 1) if num < n: return num @@ -75,7 +79,7 @@ class ModulusPack (object): # there's a bug in the ssh "moduli" file (yeah, i know: shock! dismay! # call cnn!) where it understates the bit lengths of these primes by 1. # this is okay. - bl = bit_length(modulus) + bl = util.bit_length(modulus) if (bl != size) and (bl != size + 1): self.discarded.append((modulus, 'incorrectly reported bit length %d' % size)) return @@ -123,6 +127,6 @@ class ModulusPack (object): if min > good: good = bitsizes[-1] # now pick a random modulus of this bitsize - n = roll_random(self.randpool, len(self.pack[good])) + n = _roll_random(self.randpool, len(self.pack[good])) return self.pack[good][n] diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index b797c6b9..7da13334 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -1,14 +1,23 @@ #!/usr/bin/python +""" +L{RSAKey} +""" + from message import Message from Crypto.PublicKey import RSA -from Crypto.Hash import SHA -from ber import BER +from Crypto.Hash import SHA, MD5 +from Crypto.Cipher import DES3 +from ber import BER, BERException from util import format_binary, inflate_long, deflate_long from pkey import PKey -import base64 +from ssh_exception import SSHException class RSAKey (PKey): + """ + Representation of an RSA key which can be used to sign and verify SSH2 + data. + """ def __init__(self, msg=None, data=''): self.valid = 0 @@ -68,19 +77,17 @@ class RSAKey (PKey): rsa = RSA.construct((long(self.n), long(self.e))) return rsa.verify(hash, (sig,)) - def read_private_key_file(self, filename): + def read_private_key_file(self, filename, password=None): # private key file contains: # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } self.valid = 0 - f = open(filename, 'r') - lines = f.readlines() - f.close() - if lines[0].strip() != '-----BEGIN RSA PRIVATE KEY-----': - raise SSHException('not a valid RSA private key file') - data = base64.decodestring(''.join(lines[1:-1])) - keylist = BER(data).decode() + data = self._read_private_key_file('RSA', filename, password) + try: + keylist = BER(data).decode() + except BERException: + raise SSHException('Unable to parse key file') if (type(keylist) != type([])) or (len(keylist) < 4) or (keylist[0] != 0): - raise SSHException('not a valid RSA private key file (bad ber encoding)') + raise SSHException('Not a valid RSA private key file (bad ber encoding)') self.n = keylist[1] self.e = keylist[2] self.d = keylist[3] diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index b6e18467..94168433 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -1,4 +1,18 @@ +#!/usr/bin/python -class SSHException(Exception): +""" +Exceptions defined by paramiko. +""" + + +class SSHException (Exception): + """ + Exception thrown by failures in SSH2 protocol negotiation or logic errors. + """ pass +class PasswordRequiredException (SSHException): + """ + Exception thrown when a password is needed to unlock a private key file. + """ + pass diff --git a/paramiko/transport.py b/paramiko/transport.py index ce82d75c..83ec5b05 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1,5 +1,9 @@ #!/usr/bin/python +""" +L{BaseTransport} handles the core SSH2 protocol. +""" + _MSG_DISCONNECT, _MSG_IGNORE, _MSG_UNIMPLEMENTED, _MSG_DEBUG, _MSG_SERVICE_REQUEST, \ _MSG_SERVICE_ACCEPT = range(1, 7) _MSG_KEXINIT, _MSG_NEWKEYS = range(20, 22) @@ -164,10 +168,8 @@ class BaseTransport (threading.Thread): if not self.active: return '<paramiko.BaseTransport (unconnected)>' out = '<paramiko.BaseTransport' - #if self.remote_version != '': - # out += ' (server version "%s")' % self.remote_version if self.local_cipher != '': - out += ' (cipher %s)' % self.local_cipher + out += ' (cipher %s, %d bits)' % (self.local_cipher, self._cipher_info[self.local_cipher]['key-size'] * 8) if len(self.channels) == 1: out += ' (active; 1 open channel)' else: @@ -512,6 +514,8 @@ class BaseTransport (threading.Thread): @raise SSHException: if the SSH2 negotiation fails, the host key supplied by the server is incorrect, or authentication fails. + + @since: doduo """ if hostkeytype is not None: self.preferred_keys = [ hostkeytype ] diff --git a/paramiko/util.py b/paramiko/util.py index 33b671c6..ec935cea 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -1,5 +1,9 @@ #!/usr/bin/python +""" +Useful functions used by the rest of paramiko. +""" + import sys, struct, traceback def inflate_long(s, always_positive=0): @@ -99,3 +103,37 @@ def bit_length(n): def tb_strings(): return ''.join(traceback.format_exception(*sys.exc_info())).split('\n') + +def generate_key_bytes(hashclass, salt, key, nbytes): + """ + Given a password, passphrase, or other human-source key, scramble it + through a secure hash into some keyworthy bytes. This specific algorithm + is used for encrypting/decrypting private key files. + + @param hashclass: class from L{Crypto.Hash} that can be used as a secure + hashing function (like C{MD5} or C{SHA}). + @type hashclass: L{Crypto.Hash} + @param salt: data to salt the hash with. + @type salt: string + @param key: human-entered password or passphrase. + @type key: string + @param nbytes: number of bytes to generate. + @type nbytes: int + @return: key data + @rtype: string + """ + keydata = '' + digest = '' + if len(salt) > 8: + salt = salt[:8] + while nbytes > 0: + hash = hashclass.new() + if len(digest) > 0: + hash.update(digest) + hash.update(key) + hash.update(salt) + digest = hash.digest() + size = min(nbytes, len(digest)) + keydata += digest[:size] + nbytes -= size + return keydata |